Betwise Blog
Betwise news, analysis and automatic betting info

Expected Race Duration

By Henry Young on Thursday, July 28th, 2022

When developing real time automated in-play betting strategies, variation in price is the fastest source of information available in order to infer the action in the race. This is due to the volume in betting from a large number of in-running players watching the action, some using live video drone feeds, causing prices to contract and expand depending on the perceived chances of runners in the race.

However, the interpretation of price variation caused by in-running betting depends critically on what stage the race is at. For example, a price increase at the start may be due to a recoverable stumble out of the gates, whereas price lengthening in the closing stages of a race strongly suggests a loser. This is why it is invaluable to have an estimate of the expected race duration.

Similarly, if you are building an in-play market making system, offering liquidity on both the back and lay side with a view to netting the spread, you may wish to close out or green up your positions prior to the most volatile period of the race which is typically the final furlong. Running a timer from the “off” signal using expected race duration minus, say 10 or 15 seconds, is critically important for this purpose.

A crude estimate of expected race duration can be calculated by multiplying race distance by speed typical for that distance. For example, sprints are typically paced at 11-12 seconds per furlong, whereas middle distance races are typically 13+seconds per furlong. Greater accuracy needs to account for course specifics – sharpness, undulation, uphill/downhill, etc.

Fortunately SmartForm contains a historic races field winning_time_secs. Therefore we can query for the min/max/average of this field for races at the course and distance in question. But this immediately throws up the problem that race distances in SmartForm are in yards, apparently measured with sub-furlong accuracy. If you want the data for 5f races, simply querying for 5 x 220 = 1,100 yards is not going to give you the desired result. Actual race distances vary according to the vagaries of doling out rails to avoid bad ground, how much the tractor driver delivering the starting gates had to drink the night before, etc. We need to work in furlongs by rounding the yards based distance data. Therefore we should first define a convenient MySQL helper function:

CREATE
    DEFINER='smartform'@'localhost'
FUNCTION furlongs(yards INT) RETURNS INT
    COMMENT 'calculate furlongs from yards'
    DETERMINISTIC
    NO SQL
BEGIN
    RETURN round(yards/220, 0);
END

Now we can write a query using this function to get expected race duration for 6f races at Yarmouth:

SELECT
    min(winning_time_secs) as course_time
FROM
    historic_races_beta
WHERE
    course = "Yarmouth" AND
    furlongs(distance_yards) = 6 and
    meeting_date > "2020-01-01";

With the result being:

+-------------+
| course_time |
+-------------+
|       68.85 |
+-------------+
1 row in set (0.40 sec)

The query runs through all Yarmouth races over all distances in yards which when rounded equate to 6f, calculating the minimum winning time – in effect the (recent) course record. The min could be replaced by max or avg according to needs. My specific use case called for a lower bound on expected race duration, hence using min. Note the query only uses recent races (since 01 Jan 2020) to reduce the possibility of historic course layout changes influencing results. You may prefer to tailor this per course. For example Southwell’s all weather track change from Fibersand to the faster Tapeta in 2021 would demand exclusion of all winning times on the old surface, although the use of min should achieve much the same effect.

Now that we understand the basic idea, we have to move this into the real world by making these queries from code – Python being my language of choice for coding trading bots.

The following is a Python function get_course_time to which you pass a Betfair market ID and which returns expected race duration based on recent prior race results. The function runs through a sequence of three queries which in turn:

  • Converts from Betfair market ID to SmartForm race_id using daily_betfair_mappings
  • Obtains course, race_type and distance in furlongs for the race in question using daily_races_beta
  • Queries historic_races_beta for the minimum winning time at course and distance

If there are no relevant results in the database, a crude course time estimate based on 12 x distance in furlongs is returned.

import pymysql

def get_course_time(marketId):

	query_SmartForm_raceid	= """select distinct race_id from daily_betfair_mappings
									where bf_race_id = %s"""

	query_race_parameters 	= """select course, race_type, furlongs(distance_yards) as distance from daily_races_beta
									where race_id = %s"""

	query_course_time 	= """select min(winning_time_secs) as course_time from historic_races_beta
									where course = %s and
									race_type = %s and
									furlongs(distance_yards) = %s and
									meeting_date > %s"""

	base_date = datetime(2020, 1, 1)

	# Establish connection to Smartform database

	connection = pymysql.connect(host='localhost', user='smartform', passwd ='smartform', database = 'smartform')

	with connection.cursor(pymysql.cursors.DictCursor) as cursor:

		# Initial default time based on typical fast 5f sprint

		default_course_time = 60.0

		# Get SmartForm race_id

		cursor.execute(query_SmartForm_raceid, (marketId))

		rows = cursor.fetchall()

		if len(rows):

			race_id = rows[0]['race_id']

		else:

			return default_course_time

		# Get race parameters required for course time query

		cursor.execute(query_race_parameters, (race_id))

		rows = cursor.fetchall()

		if len(rows):

			course 		= rows[0]['course']
			race_type 	= rows[0]['race_type']
			distance 	= rows[0]['distance']

		else:

			return default_course_time

		# Improved default time based on actual race distance using 12s per furlong

		default_course_time = distance * 12

		# Get minimum course time

		cursor.execute(query_course_time, (course, race_type, distance, base_date))

		rows = cursor.fetchall()

		if len(rows):

			course_time = rows[0]['course_time']

			if course_time is None:

				return default_course_time

			if course_time == 0.0:

				return default_course_time

		else:

			return default_course_time

	return course_time

Leave a comment