Using VB2008 to acccess the Betfair API: A tutorial

Collapse
This topic is closed.
X
X
 
  • Time
  • Show
Clear All
new posts
  • Mumbles0
    Junior Member
    • Jan 2009
    • 240

    #1

    Using VB2008 to acccess the Betfair API: A tutorial

    This thread is from the old forum. Because of the interest shown I am reproducing it here.

    Reading through the forum I notice that newbies often struggle when attempting to create an application to access the API. It is difficult to get past square one. This is understandable because of the myriad of complex technologies we are confronted with: Soap, Xml, Http, Wsdl, etc. Do we have to learn these technologies before we can get started? The answer is no! A relatively painless, zero-cost way in is to use a high-level language such as Microsoft’s Visual Basic 2008 to access the Free API.

    What I would like to do is demonstrate, by way of a simple exercise, how to do this, starting at square one. You will need to install VB2008 express edition, preferably with SP1, available here (this is free). You will also require an elementary knowledge of VB2008 which can be had by looking through the “Getting started” section of VB2008’s help. You will, of course, require a Betfair account. Also download and save a copy of the Betfair Sports Exchange Reference Guide v6, available here.

    Visual Basic 2010

    VB2010 is now available. This supersedes VB2008. The link now points to the download page for VB2010 Express Edition for you to download and install. The Tutorial exercises will work fine with VB2010. If there are any problems post a comment.
    Last edited by Mumbles0; 14-04-2010, 10:59 AM. Reason: VB2010 note added.
  • Mumbles0
    Junior Member
    • Jan 2009
    • 240

    #2
    Step 1. Build a Test Form

    Start VB2008. On the File menu, select New Project. Select the “Windows Forms Application” template. Enter the name “BetfairX” (or any name you like). Click OK. This creates a project with Form1 showing. In the Solution Explorer, right click on “Form1.vb” and rename it to “TestForm.vb”. Click on Form1. In the Properties panel change the Text property from “Form1” to “Betfair tester”. This gives us a more meaningful caption. Now save the project so far: On the File menu, select Save All. Browse to a convenient folder where you would like the project to be saved. I would suggest a folder named something like C:\Betfair. Click Save.

    Click the Toolbox button to show the Toolbox. From the Toolbox, drag three buttons onto the form. Click on Button1 and change its (name) property from “Button1” to “bLogin”, also change its Text property from “Button1” to “Login”. Repeat for Button2, changing its name to “bLogout” and Text property to “Logout”. Repeat for Button3, changing its name to bKeepAlive and Text property to “KeepAlive”.

    Now drag a textbox from the Toolbox onto the form. Rename it from “TextBox1” to “tLog“. Change these properties: Multiline = True, ReadOnly = True, Scrollbars = Vertical. Resize the textbox so that it occupies about two-thirds of the form. This textbox will become our “virtual printer” on which we will log messages.

    Now let’s add some code.

    On the View menu, select Code. This shows the empty Class TestForm. Within this class add the following code:

    Code:
    Sub Print(ByVal Message as String)
      With tLog
        .SelectionStart = .Text.Length
        .SelectedText = vbCrLf & Message
      End With
    End Sub
    This sub provides convenient logging of messages in the textbox. Note that as you type in this code VB2008’s “IntelliSense” comes into play, offering suggestions to complete the words. Get used to this because it is a very handy feature. It almost writes the code for you.

    Now select the TestForm.vb[Design] tab to return to the form. Double-click the Login button. This creates the empty Sub bLogin_Click. Add this line within this sub:

    Code:
    Print(“*** Login ***”)
    Repeat for the Logout button. Add this line within Sub bLogout_Click:

    Code:
    Print(“*** Logout ***”)
    Repeat for the KeepAlive button. Add this line within Sub bKeepAlive_Click:

    Code:
    Print(“*** KeepAlive ***”)
    Save your work, then test the project by clicking the Start Debugging button (the green arrow). If all is well our Betfair Tester should appear. When the buttons are clicked the corresponding messages should appear in the textbox. Note the scrolling behaviour when the textbox fills.

    We now have a simple test facility. In step 2 we will connect the buttons to the corresponding Betfair API functions.
    Last edited by Mumbles0; 03-02-2009, 11:21 PM.

    Comment

    • Mumbles0
      Junior Member
      • Jan 2009
      • 240

      #3
      Step 2. Connecting to the Global service

      The Betfair Sports Exchange API 6 Reference Guide (API Guide) informs us that the Login, Logout and KeepAlive functions are on the Global server, so we will add a reference to this service. On the Project menu select "Add Service Reference". Click "Advanced", then click "Add Web Reference" to show the “Add Web Reference” dialogue form. In the URL box type the URL of the WSDL for the Global service: https://api.betfair.com/global/v3/BFGlobalService.wsdl

      Click "Go". VB2008 now searches. If all goes well, the BFGlobalService is found. Change the Web reference name from “com.betfair.api” to “BFGlobal”. Click "Add Reference". VB2008 now adds the reference to the project and the BFGlobal icon should appear in the Solution Explorer (this can take a bit of time).

      Let’s reflect on what has just happened. Right-click on the BFGlobal icon and select "View in Object Browser" to display the Object Browser. Expand the node "BetfairX.BFGlobal" and click on the "BFGlobalService" member. Here you will see all the classes, objects, properties, enums, etc. that are listed in the API Guide. This means that the entire resources of the Global API are now available to our project. We have achieved this very easily, without having to know anything about the underlying technologies (Soap, Wsdl, Xml, Http, etc.). I find this quite amazing.

      We now add the code for the login, logout and keepAlive functions. Refer to the API Guide for full details of each call. Add this code within the TestForm class, at the top:

      Code:
      Dim oHeaderGL As New BFGlobal.APIRequestHeader 
      Dim BetfairGL As New BFGlobal.BFGlobalService
      oHeaderGL is the request header object which we will be including with all API calls (except for login). It will contain the session token. BetfairGL is the GlobalService object. It contains all of the API calling methods. Also add a new Sub. This can go anywhere within the TestForm class:

      Code:
      Sub CheckHeader(ByVal Header As BFGlobal.APIResponseHeader)
        With Header
          Print("HeaderCode = " & .errorCode.ToString)
          oHeaderGL.sessionToken = .sessionToken
        End With
      End Sub
      This sub logs the response header’s error code and saves the session token in the request header object.
      Now add more code to the existing Sub bLogin_Click so that it looks something like this:

      Code:
      Private Sub bLogin_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles bLogin.Click
        Print("*** Login ***")
        Dim oLoginReq As New BFGlobal.LoginReq
        Dim oLoginResp As BFGlobal.LoginResp
        With oLoginReq
          .username = "[i]YourUsername[/i]"
          .password = "[i]YourPassword[/i]"
          .productId = 82           'For free API
        End With
        oLoginResp = BetfairGL.login(oLoginReq)     'Call the API
        With oLoginResp
          CheckHeader(.header)
          Print("ErrorCode = " & .errorCode.ToString)
        End With
      End Sub[FONT="Courier New"][/FONT]
      This sub first creates the request object (oLoginReq) and a variable to hold the response object (oLoginResp). The request object is then loaded with the appropriate data (.username, .password and .productID for the free API), then the API is called using the login method of the BetfairGL object. When the response is received, CheckHeader logs the header’s error code and saves the session token for use by subsequent API calls. The response error code is also logged. Run the project to test it. Click the Login button. If all is well you should get two OKs, and you are logged in. Now stop the project and add more code to the KeepAlive sub:

      Code:
      Private Sub bKeepAlive_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles  bKeepAlive.Click
        Print("*** KeepAlive ***")
        Dim oKeepAliveReq As New BFGlobal.KeepAliveReq
        Dim oKeepAliveResp As BFGlobal.KeepAliveResp
        oKeepAliveReq.header = oHeaderGL
        oKeepAliveResp = BetfairGL.keepAlive(oKeepAliveReq)     'Call the API
        CheckHeader(oKeepAliveResp.header)
      End Sub
      keepAlive is the simplest API call. It can be used to test if you are logged in or not. This sub behaves similar to Login, except the request contains only the session token obtained from the Login response (in the header). Now add more code to the Logout sub:

      Code:
      Private Sub bLogout_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles bLogout.Click
        Print("*** Logout ***")
        Dim oLogoutReq As New BFGlobal.LogoutReq
        Dim oLogoutResp As BFGlobal.LogoutResp
        oLogoutReq.header = oHeaderGL
        oLogoutResp = BetfairGL.logout(oLogoutReq)     'Call the API
        With oLogoutResp
          CheckHeader(.header)
          Print("ErrorCode = " & .errorCode.ToString)
        End With
      
      End Sub
      Logout operates in a similar manner to KeepAlive, except it logs you out (whereas KeepAlive doesn’t do anything).
      Try it. Run the project and test all three functions. Notice that if you are not logged in, KeepAlive and Logout give the "NO_SESSION" response.

      In step 3 we will look at saving the session token.
      Last edited by Mumbles0; 14-04-2010, 03:27 AM. Reason: Minor change

      Comment

      • Mumbles0
        Junior Member
        • Jan 2009
        • 240

        #4
        Step 3. Saving the Session Token

        As our project now stands we must login every time we run it. Although we remain logged into to the API, we are unable to resume our previous session because the session token is lost when we stop running our project, hence we must login again in order to get a new session token. Because, during development, we tend to re-run our project frequently, this can become a pain, so it is a good idea to save the session token between runs.

        Because the session token is a string of characters, it can be saved in a simple text file. We can create a suitable file using the Windows Notepad utility. Proceed as follows (this is a once-only operation). Launch Notepad. Without entering any text, select “Save As...” on the File menu, then browse to the folder where the project is saved. I have suggested "C:\Betfair". Enter the name “SessToken” and click Save. Close Notepad. This creates the empty file "SessToken.txt" in the project folder. Now add this line to the TestForm class the near the top:

        Code:
        Const SessTokFile = "C:\Betfair\SessToken.txt"
        This is the path to the file where we will save the session token. We now add a “Form Load” event handler as follows: In the Class Name box at the top left of the code window select “TestForm Events”. In the Method Name box to the right select and click “Load”. This adds the empty Sub TestForm_Load to the class. Add a line of code to this sub:

        Code:
        Private Sub TestForm_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
          oHeaderGL.sessionToken = My.Computer.FileSystem.ReadAllText(SessTokFile)
        End Sub
        When you run the project this code is executed first. It reads the text from the SessToken.txt file into the request header object. (Initially there is no text to read, but this situation changes.). In a similar manner, create the “FormClosing” event handler. Add a line of code to this sub:

        Code:
        Private Sub TestForm_FormClosing(ByVal sender As Object, ByVal e As System.Windows.Forms.FormClosingEventArgs) Handles Me.FormClosing
          My.Computer.FileSystem.WriteAllText(SessTokFile, oHeaderGL.sessionToken, False)
        End Sub
        This sub is executed when you close TestForm. It saves the session token in the text file for subsequent use. (Note: this does not happen if you stop the project by clicking the “Stop Debugging” button.) Test it by re-running the project a few times. You can see if you remain logged in by clicking the “KeepAlive” button. If you wish, you can examine the session token using Notepad.

        In step 4 we will look at the “GetActiveEventTypes” API call.
        Last edited by Mumbles0; 03-02-2009, 11:25 PM.

        Comment

        • Mumbles0
          Junior Member
          • Jan 2009
          • 240

          #5
          Step 4. Typical API call: getActiveEventTypes

          We will now call the global getActiveEventTypes service to illustrate how data is retrieved from the API. For this we will add another button to our Betfair Tester. To do this, drag another button from the toolbox onto the form (as described in Step 1 for the Login, Logout and KeepAlive buttons). Rename this button to “bEvents” and change its Text property to “Events”. Double-click it to create the empty Sub bEvents_Click. Add the following code:

          Code:
          Private Sub bEvents_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles bEvents.Click
            Print("*** Events ***")
            Dim oEventsReq As New BFGlobal.GetEventTypesReq         'Create the request object
            Dim oEventsResp As BFGlobal.GetEventTypesResp           'Create a variable for the response object
            oEventsReq.header = oHeaderGL                               'Load request parameters
            oEventsResp = BetfairGL.getActiveEventTypes(oEventsReq)     'Call the API
            With oEventsResp
              CheckHeader(.header)	                                  'Check response header
              Print("ErrorCode = " & .errorCode.ToString)
              If .errorCode = BFGlobal.GetEventsErrorEnum.OK Then       'Check the response errorcode
                For i = 0 To .eventTypeItems.Length - 1                'Process the received data
                  With .eventTypeItems(i)
                    Print(.name & " (" & .id & ")")
                  End With
                Next
              End If
            End With
          End Sub
          Try it. When you click the “Events” button a list of event types should appear with the event Id in brackets.
          This code demonstrates the pattern which applies to all API calls:

          1. Create the request object and a variable to hold the response object from the appropriate classes.

          2. Load the request object with the request header (which contains the session token), and any parameters required by the API as detailed in the API guide for the particular call. In this case, no other parameters are required.

          3. Call the API. The syntax is Response = APImethod(Request). The API acts on the request and returns the data in the response object.

          4. Check the response header. This is a standard procedure.

          5. Check the response error code. If OK, process the received data.
          In this case the EventType data is returned in the .eventTypeItems array.

          VB2008 has a nice feature which allows us to examine the contents of objects (called DataTips). Let’s look at this. Put a breakpoint on the “With oEventsResp” statement. (Simply click on the margin to the left of this statement.). Run the project and click “Events”. Execution stops on this statement. Move the mouse cursor over the word “oEventsResp“. The small window appears immediately under this word. Carefully move the cursor into this window and the entire structure of the object will open up, showing the contents of all properties. Click on the "+" sign to open an object property. This is very useful for examining arrays containing large amounts of data, which is often the case.

          In step 5 we will look at accessing an Exchange API.
          Last edited by Mumbles0; 21-06-2009, 02:48 AM. Reason: Minor change

          Comment

          • Mumbles0
            Junior Member
            • Jan 2009
            • 240

            #6
            Step 5. Accessing an Exchange API

            Most of the action occurs on Betfair’s UK Exchange API. We will now add the facility to access this. In step 2 we added a reference to the Global API. We proceed in a similar manner. On the Project menu, select “Add Web Reference” to show the “Add Web Reference” dialogue form. (If this menu item is not there, proceed as per step 2.). N.B. Do not use the “Add Service Reference” dialogue. Add the URL for the UK Exchange service: https://api.betfair.com/exchange/v5/BFExchangeService.wsdl

            Click “Go“. The BFExchangeService should now be found. Change the Web reference name to “BFUK”. Click “Add Reference”. After a while the “BFUK” icon should appear in the Solution Explorer near the existing “BFGlobal” icon. If you look again in the Object Browser you will see that the entire resources of the UK Exchange API are now available to our project.

            Add this line near the top of the TestForm class:

            Code:
            Dim BetFairUK As New BFUK.BFExchangeService     'The UK ExchangeService object
            Now a problem arises. All requests to BetfairUK require a request header (as per BetfairGlobal). We already have an object for this purpose, oHeaderGL, in which we are storing the session token. It would be nice if we could assign this to the .header property of these requests, but if we attempt to do this we get a “type mismatch” compiler error. This is because the oHeader object is type BFGlobal.APIRequestHeader, but the compiler requires an object of type BFUK.APIRequestHeader. Even though these classes are identical, the compiler does not know this. So we create a compatible header object by adding this function:

            Code:
            Function oHeaderUK() As BFUK.APIRequestHeader
              Dim Header As New BFUK.APIRequestHeader
              Header.sessionToken = oHeaderGL.sessionToken
              Return Header
            End Function
            For the same reason, the existing CheckHeader sub will not work for response headers from calls to BetfairUK. So we add another CheckHeader sub:

            Code:
            Sub CheckHeader(ByVal Header As BFUK.APIResponseHeader)
              With Header
                Print("HeaderCode = " & .errorCode.ToString)
                oHeaderGL.sessionToken = .sessionToken
              End With
            End Sub
            Note that we now have two CheckHeader subs which are identical except for the parameter type. The compiler selects the appropriate one. (This is called “overloading”.)
            We will now call the getAllMarkets service to illustrate how data is obtained from BetfairUK. To do this, add another button to the Betfair Tester (you should be familiar with how to do this by now). Rename this button “bMarkets”, and change its text property to “Markets”. Double-click it to create the event handler sub bMarkets_Click. Add this code:

            Code:
            Private Sub bMarkets_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles bMarkets.Click
              Print("*** Markets ***")
              Dim oMarketsReq As New BFUK.GetAllMarketsReq
              Dim oMarketsResp As BFUK.GetAllMarketsResp
              With oMarketsReq
                .header = oHeaderUK()
                ReDim .eventTypeIds(0) : .eventTypeIds(0) = 7  'For horse racing
                ReDim .countries(1) : .countries(0) = "GBR" : .countries(1) = "ZAF"
                .fromDate = Today
                .toDate = Today.AddDays(1)
              End With
              oMarketsResp = BetFairUK.getAllMarkets(oMarketsReq)  'Call the UK API
              With oMarketsResp
                CheckHeader(.header)
                Print("ErrorCode = " & .errorCode.ToString)
                If .errorCode = BFUK.GetAllMarketsErrorEnum.OK Then
                  Print(.marketData)
                End If
              End With
            End Sub
            This sub performs the usual calling sequence. For this call several optional request parameters can be specified as detailed in the API Guide. Fortunately VB2008’s data types are compatible. Two parameters (.eventTypeIds, .countries) require an array because multiple values can be used. The code shows an easy way of setting up arrays for this purpose. eventTypeIds are obtained from the getAllActiveEventTypes call which we looked at in Step 4 (7 is horse racing). The two Date parameters (.fromDate, .toDate) will accept any of VB2008’s Date methods. Note that a Date value also contains a time component. The example specifies the time range from the start of today to the start of tomorrow.

            Try it. The returned data is shown in the Textbox. Unlike getActiveEventTypes which we looked at in Step 4, this call returns a packed string (.marketData) containing the requested data. This is unfortunate because we now require extra code to unpack this string into a usable form for subsequent processing. In Step 6 we look at a way doing this.


            Accessing the Australian Exchange API.

            Betfair’s Australian Exchange API is used primarily for Australian sports markets. To access the Australian Exchange we proceed in exactly same way as for the UK Exchange described above, except when we create the Web Reference use the URL:
            https://api-au.betfair.com/exchange/v5/BFExchangeService.wsdl. Name this reference “BFAU“. In the code samples substitute all occurrences of “AU” for “UK” This works because both Exchanges are identical in operation, and BFAU contains the same set of classes as BFUK.
            Last edited by Mumbles0; 21-06-2009, 03:07 AM. Reason: Minor change

            Comment

            • Mumbles0
              Junior Member
              • Jan 2009
              • 240

              #7
              Step 6. Unpacking Response Strings

              Some of the API calls return data as packed strings rather than easy-to-use objects containing data arrays. I am not exactly sure why, but I suspect it is to reduce the size of the response file because these calls can return large amounts of data. A smaller file takes less time to receive. We now look at unpacking the simple string returned by the getAllMarkets call we made in step 5. For this we will use a new code module. On the Project menu select "Add New Item", then select "Module" from the list of templates. Enter the name "Unpack". Click "Add". Add the following code within this newly-created module:

              Code:
              Class MarketDataType           'For getAllMarkets data
                Public marketId As Integer
                Public marketName As String
                Public marketType As String
                Public marketStatus As String
                Public eventDate As DateTime
                Public menuPath As String
                Public eventHeirachy As String
                Public betDelay As Integer
                Public exchangeId As Integer
                Public countryCode As String
                Public lastRefresh As DateTime
                Public noOfRunners As Integer
                Public noOfWinners As Integer
                Public totalAmountMatched As Double
                Public bspMarket As Boolean
                Public turningInPlay As Boolean
              End Class
              This class defines the data structure for each market. Referring to the “Get All Markets” description in the API Guide, note how the members of this class match the items in the “marketData” table. Now add code for another class within this module as follows:

              Code:
              Class UnpackAllMarkets       'For getAllMArkets
                Public marketData As MarketDataType() = {}  'The returned array of market data
                Private Const BaseDate As DateTime = #1/1/1970#
                Private Const ColonCode = "&%^@"  'The substitute code for "\:"
              
                Sub New(ByVal MarketString As String)
                  Dim n As Integer, Mdata, Field As String()
              
                  Mdata = MarketString.Replace("\:", ColonCode).Split(":") 'Get array of Market substrings
                  n = UBound(Mdata) - 1
                  ReDim marketData(n)
              
                  For i = 0 To n
                    Field = Mdata(i + 1).Replace("\~", "-").Split("~") 'Get array of data fields
                    marketData(i) = New MarketDataType
                    With marketData(i)
                      .marketId = Field(0)   'Load the array items
                      .marketName = Field(1).Replace(ColonCode, ":")
                      .marketType = Field(2)
                      .marketStatus = Field(3)
                      .eventDate = BaseDate.AddMilliseconds(Field(4))
                      .menuPath = Field(5).Replace(ColonCode, ":")
                      .eventHeirachy = Field(6)
                      .betDelay = Field(7)
                      .exchangeId = Field(8)
                      .countryCode = Field(9)
                      .lastRefresh = BaseDate.AddMilliseconds(Field(10))
                      .noOfRunners = Field(11)
                      .noOfWinners = Field(12)
                      .totalAmountMatched = Val(Field(13))
                      .bspMarket = (Field(14) = "Y")
                      .turningInPlay = (Field(15) = "Y")
              
                    End With
                  Next
                End Sub
               End Class
              This class does the unpacking. The unpacked data will be returned in the .marketData array. This array is initialized to “empty”. BaseDate holds the TimeDate representation of 1 January 1970 00:00:00 GMT. This is used to convert .eventTime and .lastRefresh because these values are returned as milliseconds elapsed since this date. (Don’t ask me why.)

              Sub New (called the constructor) is invoked whenever an object is created from this class. It behaves like any other sub. The MarketString parameter contains the packed data.

              If we look closely at the packed string we see that the data for each market is delimited by a preceding colon (“:”), with individual parameter fields delimited by “~”. Just to make things difficult, a colon can occur in the data. For example .marketName could include a time (e.g. “R5 2:15pm 7f Hcap”). If we don’t cater for this possibility the colon will be interpreted as an end-of-market string delimiter, and the code will fail. In the API Guide Betfair assure us that, if this happens, the API will add a preceding “escape” character (“\”) to allow this situation to be detected (e.g. “R5 2\:15pm 7f Hcap”). So we use the .Replace method to replace all occurrences of “\:” with an arbitary ColonCode string ("&%^@") which is unlikely to occur in the data. Note that a similar situation could occur with the “~” delimiter, however I think this is very unlikely. To keep thing simple we simply substitute “-” for “\~”.

              The .Split method uses ":" to split MarketString into an array of market data substrings which we assign to Mdata(). Note that element 0 is empty because the first character of MarketString is a colon. In a similar manner we now split each market substring into individual data substrings which are held in the Field() array, using the “~” delimiter.

              The returned parameter substrings are now routinely converted and assigned to the corresponding properties of the .marketData array. Sould it exist, the ColonCode is replaced by “:” in the .marketName and .menuPath parameters. .eventDate and .lastRefresh are converted to DateTime values using the .AddMilliseconds method. The Boolean values .bspMarket and .turningInPlay are converted as shown. The Double value, .totalAmountMatched, is converted simply using the Val function. Integer parameters use the default conversion.

              Now test this code. Return to the TestForm code. In Sub bMarkets_Click locate this statement (which we added in step 5):

              Print(.marketData)

              Replace this statement with the following code:

              Code:
                
              Dim AllMarkets As New UnpackAllMarkets(.marketData)   ‘Create an object and unpack the string
              With AllMarkets
                For i = 0 To .marketData.Length - 1
                  With .marketData(i)
                    Print(.marketID & " " & .marketStatus & "  " & .marketName & “  “ & .menuPath)
                  End With
                Next
              End With
              Here the Dim statement creates an AllMarkets object from the UnpackAllMarkets class. In doing so, data in the oMarketsResp.marketData string is unpacked into the .marketData array within the AllMarkets object.

              Run the project to test this. When you click the “Markets” button the selected properties should be printed in the TextBox. Change the Print statement to show other properties if you wish. All data that was packed into the response string is now available to us in a more convenient form. To better understand how things work step through the code using the “step” functions of the debugger.

              Other “compressed” API calls such as GetMarketPricesCompressed can be handled in a similar manner. We look at unpacking this at a later stage.
              Last edited by Mumbles0; 14-04-2010, 03:37 AM. Reason: Minor change

              Comment

              • Mumbles0
                Junior Member
                • Jan 2009
                • 240

                #8
                Step 7. Restrictions of the Free API.

                If you are using the Free API some restrictions apply. Not all API calls are available, and some have calling rate limits. Despite this, it is possible to develop a useful Betfair client application for private use using the free API. A list of restrictions is given in the API product comparison table here.

                The calling rate restrictions force us into economical use of the API. Even if you are using the Full Access API there is no virtue in “hammering” the server with unnecessary requests. Note that charges apply for high usage (20 calls/sec or more) , however this will not concern us now.

                For example, the table shows that GetMarketPrices cannot be called more than 10 times per minute. We now investigate this call. For the exercise we will restructure the calling sequence so that separate methods contain the request and response code. Add the following code to the TestForm class:

                Code:
                Function MpricesReq() As BFUK.GetMarketPricesReq
                  Dim oMPReq As New BFUK.GetMarketPricesReq
                  With oMPReq
                    .header = oHeaderUK()
                    .marketId = [i]an active market ID[/i]
                  End With
                  Return oMPReq
                End Function
                This function returns a request object for a GetMarketPrices API call. To test this you will have to use a valid market ID for a currently active market. Run the Betfair Tester and select a market ID from the list produced when the ‘Markets” button is clicked. (The market ID is the integer number.) Assign this to the .marketId property. Now add the following code:

                Code:
                Sub ShowMprices(ByVal MpriceResp As BFUK.GetMarketPricesResp)
                  Dim Lay, Back As String
                  With MpriceResp
                    CheckHeader(.header)
                    Print("ErrorCode = " & .errorCode.ToString)
                    If .errorCode = BFUK.GetMarketPricesErrorEnum.OK Then
                      With .marketPrices
                        Print("MarketID = " & .marketId)
                        For i = 0 To .runnerPrices.Length - 1
                          With .runnerPrices(i)
                            Print("Runner " & i + 1 & "  LPM = " & .lastPriceMatched)
                            Back = ""
                            For j = 0 To .bestPricesToBack.Length - 1
                              With .bestPricesToBack(j)
                                Back = Back & "  " & .price & "/" & Int(.amountAvailable)
                              End With
                            Next
                            Lay = ""
                            For j = 0 To .bestPricesToLay.Length - 1
                              With .bestPricesToLay(j)
                                Lay = Lay & "  " & .price & "/" & Int(.amountAvailable)
                              End With
                            Next
                            Print("Back = " & Back & "   Lay = " & Lay)
                          End With
                        Next
                      End With
                    End If
                  End With
                End Sub
                This sub displays data returned from a GetMarketPrices API call in the TextBox. The prices and amounts currently available for each runner are listed (as odds/amount). The last price matched (LPM) is also shown. Now add another button to the Betfair Tester. Rename this button “bPrices”, and change its text property to “Prices”. Double-click it to create the event handler Sub bPrices_Click. Add this code:

                Code:
                Private Sub bPrices_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles bPrices.Click
                  Print("*** Prices ***")
                  ShowMprices(BetFairUK.getMarketPrices(MpricesReq))    'Get market prices
                End Sub
                This sub is now quite brief because the MpricesReq function supplies the request object and the ShowMprices sub processes the response. Try it. Run the project and click the “Prices” button. If all is OK, price data for the given market will appear in the TextBox. Now click this button continuously. After a while you will probably get the EXCEEDED_THROTTLE message. This occurs when the server detects that you are sending requests at a rate more than the Free API allows. There is no harm done, but excessive requests are rejected by the API.

                In Step 8 we will use a timer to control the calling rate.

                Comment

                • Mumbles0
                  Junior Member
                  • Jan 2009
                  • 240

                  #9
                  Step 8. Using a Call Timer

                  For calls that are frequently made, for example to get market price updates, a timer component can be used. Proceed as follows. From the Toolbox drag a timer onto the Betfair Tester form. Click on this to select it in the properties window. Change its name from “Timer1” to “PriceTimer”. Double-click on the PriceTimer icon to create the event handler PriceTimer_Tick. Add this code:

                  Code:
                  Private Sub PriceTimer_Tick(ByVal sender As Object, ByVal e As System.EventArgs) Handles PriceTimer.Tick
                    Print("*** Prices(timer) ***")
                    ShowMprices(BetFairUK.getMarketPrices(MpricesReq))  'Get market prices
                  End Sub
                  This code is similar to the “Prices” button click event. It automatically calls GetMarketPrices whenever the timer “ticks” (i.e. periodically times out). To control the timer add another button to the TestForm. Name it “bPricesT” and change its text property to “Prices(timer)”. Double-click it to create the event handler Sub bPricesT_Click. Add this code:

                  Code:
                  Private Sub bPricesT_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles bPricesT.Click
                    With PriceTimer
                      If .Enabled Then  'Timer is running
                        .Stop()
                        bPricesT.ForeColor = Color.Black
                      Else  'Timer is stopped
                        .Interval = 6000   'For 6-sec time interval
                        .Start()
                        bPricesT.ForeColor = Color.Green
                      End If
                    End With
                  End Sub
                  The “Prices(timer)” button starts and stops the timer. The text colour changes when the timer is running. To achieve 10 calls/sec (The Free API limit for GetMarketPrices) we require a period of 6 sec, so we set the timer’s interval property to 6000 milliseconds just before we start it. Test this by running the project. Ensure that you have an active market ID in the MpricesReq function. After you click the “Prices(timer)“ button you should see a price update every 6 seconds.

                  Experiment with the “Interval” property. Reduce it and observe what happens. To learn more about the Timer component, look in VB2008’s Help.

                  Comment

                  • Mumbles0
                    Junior Member
                    • Jan 2009
                    • 240

                    #10
                    Step 9. Making Async API Calls.

                    A simple VB2008 project such as the Betfair tester, can be called a single-thread, event-driven process. When running, it simply waits for an event to occur. An event occurs when you click a button. This causes execution of the button’s event handler which initiates the following sequence: A request is computed and sent to the API server via the internet. The server retrieves the requested information from its database and assembles a response. This response is sent back to us via the internet. You will have observed by now that this takes a noticeable amount of time (from a few hundred milliseconds to several seconds). During this time, the execution thread of our process waits patiently for the response to return. If another event occurs during this time, processing of the new event must wait until the response is fully received and processed. In fact, no other processing can occur during the wait. To demonstrate this effect we will conduct a simple experiment. Add a “Beep” statement as the first line in the existing Sub KeepAlive_Click:

                    Code:
                     Beep()
                    Run the project (ensure that you have an active market ID in the MpricesReq function). When you click the “KeepAlive” button you should hear a beep through your speakers. Now wait a while, then click “Prices”, immediately followed by “KeepAlive”. You may notice that the beep is delayed until the price response has been received. This is because execution of the “KeepAlive” event cannot proceed until the “Prices” event has finished processing. Because a practical Betfair client application will make frequent API calls, this delay can become a problem because a large percentage of the available process time is spent waiting for API responses. Your project becomes sluggish, inefficient and disappointing. To overcome this, use asynchronous API calls.

                    Display the Object Browser. Expand the node BetfairX.BFUK and click on BFExchangeService. Here you see a list of all API calling methods. Notice that there is an “Async” version for all calls (actually there are two). We will now make a “GetMarketPricesAsync” call. Add another button to the TestForm. Name it “bPricesA” and change its text property to “Prices(async)”. Double-click it to create its event handler Sub bPricesA_Click. Add this code:

                    Code:
                    Private Sub bPricesA_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles bPricesA.Click
                      Print("*** Prices(async) ***")
                      BetFairUK.getMarketPricesAsync(MpricesReq)       'Send the request
                    End Sub
                    This code simply initiates the request. It executes very quickly because no waiting is required. Try it out. Nothing happens because we have not yet added code to handle the response. We do this now. Locate the BetFairUK declaration statement near the top of the TestForm code and add the “WithEvents” keyword so that it becomes:

                    Code:
                    Dim WithEvents BetFairUK As New BFUK.BFExchangeService     'The UK ExchangeService object
                    In the Class Name box select “BetFairUK”. In the Method Name box click on “getMarketPricesCompleted”. This creates the empty event handler Sub BetFairUK_getMarketPricesCompleted. Add this code:

                    Code:
                    Private Sub BetFairUK_getMarketPricesCompleted(ByVal sender As Object, ByVal e As BFUK.getMarketPricesCompletedEventArgs) Handles BetFairUK.getMarketPricesCompleted
                      Try
                        If Not e.Cancelled Then ShowMprices(e.Result)
                      Catch ex As ApplicationException
                        Print(e.Error.Message)
                      End Try
                    End Sub
                    As the name suggests, this sub is is executed automatically when a response is received from the API. The “e” parameter contains (among other things) the response object (in the .result property). The response data is processed by calling the ShowMprices sub similar to the “Prices“ event. Note the “Try-Catch” block. This will print an error message if a problem occurs with the call. Run the project. Click the “Prices(async)” button (once only). If all is OK, the market prices are printed.

                    Now repeat the simple experiment above by clicking “Prices(async)” immediately followed by “KeepAlive”. Notice that, unlike previously, the “KeepAlive” event is not delayed at all. It now executes before the MarketPrices response is received.

                    Now click “Prices(async)” twice in quick succession. An error occurs because we have sent a new request before a response has been received for the previous request. We can successfully do this if we use the second version of the async call. Add this line near the top of the TestForm code:

                    Code:
                    Private StateCount As Integer    'The unique state object
                    Also modify the bPricesA_Click event handler so that is becomes:

                    Code:
                    Private Sub bPricesA_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles bPricesA.Click
                      Print("*** Prices(async) ***")
                      StateCount += 1   'Increment
                      BetFairUK.getMarketPricesAsync(MpricesReq, StateCount)  'Send the request
                    End Sub
                    Here we use the alternate version of the async call. The counter is incremented to provide a unique value for each call. Run the project and click “Prices(async)” a few times. Notice that the API is quite capable of handling “overlapping” requests.

                    That’s it. In this step we have seen that an async call requires two quick events, whereas a “standard’ (sync) call executes in a single event handler which must wait for the response. It is important to realize that async calls do not speed up API response times, but they provide far more efficient use of process time, allowing your application to quickly respond to other events and perform other processing tasks instead of waiting for API responses. In my view, all calls that are made frequently or have long response times should be async. The slight increase in code complexity is well worth the effort.
                    Last edited by Mumbles0; 04-02-2009, 12:23 AM.

                    Comment

                    • Mumbles0
                      Junior Member
                      • Jan 2009
                      • 240

                      #11
                      Review of Steps so far

                      So far we have covered the basic techniques for accessing the API. If you have completed the exercises you should have enough knowledge to produce an application of your own creation, be it bot, bet assistant or whatever. What you do with the data returned from the API is entirely up to you. To present the returned data VB2008 has many components worth considering. These include Textbox and Button (which we have used). There are others such as ListBox, CheckBox, NumericUpDown, MenuStrip, ToolStrip, DataGridView, Panel, etc. There are also graphics methods.

                      The basic steps are now complete. I will add more if I think of a topic that might be of interest. Currently I am planning to add a more versatile unpacking scheme (following on from Step 6). Beyond this, I am open to suggestions

                      Comment

                      • Mumbles0
                        Junior Member
                        • Jan 2009
                        • 240

                        #12
                        Step 10. Calling getCompleteMarketPricesCompressed.

                        In this step we call getCompleteMarketPricesCompressed to provide an example of unpacking a complex string response. This call returns market data somewhat similar to getMarketPrices (called in Step 9). It is a useful call because it returns the complete array of prices for all runners, and can be called once per second using the free API. For this we will create another code module. On the Project menu select “Add Module“. Enter the name “Unpack2”. Click “Add”. Add the following code within the newly-created module:

                        Code:
                        Class RunnerInfoType
                          Public selectionId As Integer
                          Public sortOrder As Integer
                          Public totalAmountMatched As Double
                          Public lastPriceMatched As Double
                          Public handicap As Double
                          Public reductionFactor As Double
                          Public vacant As Boolean
                          Public asianLineId As Integer
                          Public farBSP As Double
                          Public nearBSP As Double
                          Public actualBSP As Double
                          Public prices As PricesType()
                        End Class
                        
                        Class PricesType
                          Public price As Double
                          Public backAmount As Double
                          Public layAmount As Double
                          Public totalBspBackAmount As Double
                          Public totalBspLayAmount As Double
                        End Class
                        These 2 classes define the required data structures. If you refer to the API guide for this call you will notice how these properties correspond to the items given in the “marketPrices” table. Where possible I have used parameter names that are consistent with getMarketPrices. Now add the code to unpack the response string:

                        Code:
                        Class UnpackCompleteMarketPricesCompressed
                          Public marketId As Integer
                          Public delay As Integer
                          Public removedRunners As String
                          Public runnerInfo As RunnerInfoType() = {}
                          Private Const ColonCode = "&%^@"     'The substitute code for "\:"
                        
                          Sub New(ByVal MarketPrices As String)       'Unpack the data string 
                            Dim Mprices, Field, Part As String(), k, n, m As Integer
                        
                            Mprices = MarketPrices.Replace("\:", ColonCode).Split(":")      'Split the runner data fields
                            Field = Mprices(0).Replace("\~", "-").Split("~")  'Split the market data fields
                            marketId = Field(0)  'Assign the market data
                            delay = Field(1)
                            removedRunners = Field(2).Replace(ColonCode, ":")
                        
                            n = UBound(Mprices) - 1
                            ReDim runnerInfo(n)
                            For i = 0 To n      'For each runner
                              Part = Mprices(i + 1).Split("|")     'Split runner string into 2 parts
                              Field = Part(0).Split("~")        'Split runner info fields
                              runnerInfo(i) = New RunnerInfoType
                              With runnerInfo(i)        'Assign the runner parameters
                                .selectionId = Field(0)
                                .sortOrder = Field(1)
                                .totalAmountMatched = Val(Field(2))
                                .lastPriceMatched = Val(Field(3))
                                .handicap = Val(Field(4))
                                .reductionFactor = Val(Field(5))
                                .vacant = (Field(6).ToLower = "true")
                                .asianLineId = Field(7)
                                .farBSP = Val(Field(8))
                                .nearBSP = Val(Field(9))
                                .actualBSP = Val(Field(10))
                        
                                Field = Part(1).Split("~")   'Split price info
                                m = UBound(Field) \ 5 - 1   'm = number of prices - 1 
                                ReDim .prices(m) : k = 0
                                For j = 0 To m
                                  .prices(j) = New PricesType
                                  With .prices(j)      'Load the price array
                                    .price = Val(Field(k + 0))
                                    .backAmount = Val(Field(k + 1))
                                    .layAmount = Val(Field(k + 2))
                                    .totalBspBackAmount = Val(Field(k + 3))
                                    .totalBspLayAmount = Val(Field(k + 4))
                                    k += 5
                                  End With
                                Next
                              End With
                            Next
                          End Sub
                        End Class
                        When an object is created from this class, Sub New is called with the string MarketPrices containing the packed data. As discussed in Step 6, if "\:" appears in the string it is not to be treated as a delimiter, so we replace it with ColonCode to avoid a possible problem.

                        The MarketPrices input string is split using ":" into the Mprices array. Mprices(0) is split using "~" into its component fields .marketId, .delay and .removedRunners. The remaining elements contain info for each runner. Each element is split into 2 parts using "|". Part(0) contains the runner info fields (.selectionId, .sortOrder, etc.) while Part(1) contains the price data which is split using "~" and loaded into the .prices array as shown.

                        To test this code add another button to the TestForm. Name it “bCompletePrices” and change its text property to “Complete Prices”. Double-click it to create its event handler Sub bCompletePrices_Click. Add this code:

                        Code:
                        Private Sub bCompletePrices_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles bCompletePrices.Click
                          Print("*** Complete Market Prices ****")
                          Dim oCMPCreq As New BFUK.GetCompleteMarketPricesCompressedReq
                          With oCMPCreq
                            .header = oHeaderUK()
                            .marketId = [i]YourMarketIdOfInterest[/i]   'an active market ID
                          End With
                          StateCount += 1
                          BetFairUK.getCompleteMarketPricesCompressedAsync(oCMPCreq, StateCount)  'Call the API
                        End Sub
                        Use a valid market ID for a currently active market. Note that this is an async call which requires an event handler. In the Class Name box select “BetFairUK”. In the Method Name box click on “getCompleteMarketPricesCompressedCompleted”. Add this code:

                        Code:
                        Private Sub BetFairUK_getCompleteMarketPricesCompressedCompleted(ByVal sender As Object, ByVal e As BFUK.getCompleteMarketPricesCompressedCompletedEventArgs) Handles BetFairUK.getCompleteMarketPricesCompressedCompleted
                          Try
                            If Not e.Cancelled Then
                              With e.Result
                                CheckHeader(.header)
                                Print("ErrorCode = " & .errorCode.ToString)
                                If .errorCode = BFUK.GetCompleteMarketPricesErrorEnum.OK Then
                                  Dim oMarketPrices As New UnpackCompleteMarketPricesCompressed(.completeMarketPrices)
                                  With oMarketPrices
                                    Print(.marketId & "  " & .runnerInfo.Length & " runners")
                                    'Process returned market prices here.
                                    For i = 0 To .runnerInfo.Length - 1
                                      With .runnerInfo(i)
                                        Print(.sortOrder & " " & .selectionId & " " & .totalAmountMatched & " " & .lastPriceMatched)
                                      End With
                                    Next
                                  End With
                                End If
                              End With
                            End If
                          Catch ex As ApplicationException
                            Print(e.Error.Message)
                          End Try
                        End Sub
                        This sub executes when the response is received. The usual checks are performed and, if all is OK, the oMarketPrices object is created containing the unpacked market data for your application to process. We print a few parameters to demonstrate.

                        If Betfair revise the structure of the packed data in future (for example, adding a new data field), it should be relatively easy to accommodate any changes.
                        Last edited by Mumbles0; 11-04-2009, 03:10 PM. Reason: Code revised to use String.Split method.

                        Comment

                        • Mumbles0
                          Junior Member
                          • Jan 2009
                          • 240

                          #13
                          Step 11. A Look at Multithreading.

                          I’m sure that most of us have heard of multithreading, but not everyone will know what it is. Although this is an advanced topic, we can walk through a simple exercise to get a fundamental understanding of the multithread concept.

                          For this we create a new project (proceeding as per step 1). Start VB2008. On the File menu, select New Project. Enter the name “ThreadX“. OK. In the Solution Explorer right-click on “Form1.vb” and rename it to “ThreadForm.vb” Select the form and change it’s Text property to “ThreadX”. From the Toolbox drag two buttons onto the form. Rename Button1 to “bStart” and change it’s Text property to “Start”. Rename Button2 to “bStop” and change it’s text property to “Stop”. Now view the code for the form. Enter code so that it becomes:

                          Code:
                          Imports System.Threading
                          Public Class Threadform
                          
                            Private StopFlag As Boolean
                          
                            Sub DoTheWork()
                              Do                                    ‘The loop
                              Loop Until StopFlag
                              Beep()
                            End Sub
                          
                          End Class
                          The Imports statement provides access to classes in the System.Threading namespace. Sub DoTheWork represents a lengthy processor-intensive routine. The loop executes indefinitely until StopFlag is set True.

                          In the Class Name box select bStart. In the Method Name box select Click. This adds the event handler for the Start button. Add the following code:

                          Code:
                          Private Sub bStart_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles bStart.Click
                            StopFlag = False
                            bStart.Enabled = False
                            DoTheWork()
                          End Sub
                          This sub calls DoTheWork. The appearance of the Start button changes to indicate that DoTheWork is executing. Repeat for the Stop button. Include this code:

                          Code:
                          Private Sub bStop_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles bStop.Click
                            StopFlag = True
                            bStart.Enabled = True
                          End Sub
                          When the Stop button is clicked this sub sets StopFlag. This, we hope, will terminate DoTheWork.

                          Now run the project. Click the Start Button. DoTheWork starts executing. Now click the Stop button. It doesn‘t work! The project is out of control! This problem has arisen because DoTheWork shares a single thread of execution with the system code which monitors the buttons. Because this thread is completely “tied up” in the loop within DoTheWork, it cannot respond to a Stop button click, hence StopFlag can never be set and the loop will execute forever. To solve this problem we put DoTheWork onto a separate thread by modifying the bStart_Click code so that it becomes:

                          Code:
                          Private Sub bStart_Click(ByVal sender As Object, ByVal e As System.EventArgs) Handles bStart.Click
                            StopFlag = False
                            bStart.Enabled = False
                            Dim Fred As New Thread(AddressOf DoTheWork)
                            Fred.Start()
                          End Sub
                          So here, instead of calling DoTheWork directly, we first assign it to a new thread (Fred) and then start this thread. DoTheWork will now execute on a separate thread. When you run the project it should behave as expected - the Stop button now stops execution of the loop. The primary thread (which monitors the buttons) and the second thread (which executes DoTheWork) are said to execute concurrently.

                          This example may be trivial but it does demonstrate the basics of multithreading. How could multithreading be used in an API application?

                          Although you may not have been aware, we have already used multithreading in Step 9. When we make an async call, the waiting for the response is performed on another thread supplied by the system. This process is handled “beneath the surface” by .NET and is transparent to you, the user. We could have written code to relegate each standard (sync) API call to it’s own separate thread, raising an event when the response is received, but why bother, the async call does exactly this.

                          Multithreading is more applicable to larger multi-user commercial systems, than it is to simple applications, such as the Betfair Tester. However, if your project has a major time-consuming process (for example, a lengthy data analysis task) it may be well worthwhile executing this on a separate thread in a manner similar to Sub DoTheWork, thus allowing your application to remain responsive to other events while this task is executing.

                          Multithreading is a very large field and we have only just “scratched the surface” in this exercise. If you are considering using more than one thread in your project there is a component in the Toolbox called BackgroundWorker that you may find useful. Also, with VB2008 you can access the large range of multithreading facilities that .NET provides.
                          Last edited by Mumbles0; 12-02-2009, 06:21 AM.

                          Comment

                          • John Moore
                            Junior Member
                            • Feb 2009
                            • 3

                            #14
                            Hi,
                            Thanks for the thread, it`s helped my understanding of the Betfair API no end. I have a couple of problems which I cant solve.

                            1. Step 10 just gives the following response?

                            *** Market Prices ****
                            HeaderCode = OK
                            ErrorCode = OK
                            100367511 7 runners

                            I assume I`ve made a error somewhere. Can you help?

                            2. I guess this must be an obvious question... How do I place a bet?

                            Best regards,
                            John Moore

                            Comment

                            • Mumbles0
                              Junior Member
                              • Jan 2009
                              • 240

                              #15
                              Reply to John ....

                              1. Re: Step 10.

                              Success! I don't think you've made an error. That’s all you get.

                              The intended purpose of the step was to construct the “oMarketPrices” object containing all data returned from the getCompleteMarketPricesCompressed call. Because this object contains a large amount of data we only print MarketId and the length of the RunnerInfo array (which equals the number of runners) to keep things simple. This object contains a lot more than this.

                              If you wish you can inspect the contents of this object by placing a breakpoint on the “With oMarketPrices” statement. Execution will stop here when you click the “MarketPrices” button. Hover your mouse over the word “oMarketPrices” and you can examine the entire contents of this object using “DataTips” (as mentioned in step 4).

                              You can print more data if you wish. For example, extend the “With oMarketPrices” bracket as follows:

                              Code:
                              With oMarketPrices
                                Print(.MarketID & "  " & .RunnerInfo.Length & " runners")
                                'Process returned market prices here.
                                For i = 0 To .RunnerInfo.Length - 1
                                  With .RunnerInfo(i)
                                    Print(.SelectionID & " " & .LastPriceMatched.ToString("f2"))
                                  End With
                                Next
                              End With
                              This also prints the selection ID and last price matched for each runner in the market. There is lots more data in the object for you to use if required (see the API Guide). It is up to you how you present this data in your application.

                              2. Placing a bet

                              Placing a bet is fairly straightforward. Once you know your MarketId, a typical procedure might be as follows:

                              1. Call GetMarkets. This returns an array containing the Name and SelectionId for each runner. From this, make your selection.

                              2. Call PlaceBets to place the bet. Specify your SelectionId and details of the bet.

                              3. Call GetMUBets at regular intervals (say, once per second) to determine if/when your bet is matched.

                              I am thinking about making bet placement the subject of a future step.
                              Last edited by Mumbles0; 19-02-2009, 10:00 AM.

                              Comment

                              Working...
                              X