Beat the Odds with Betacus
Table of Contents
- Background
- Horror
- Setting Up
- Chakra
- An SVG
- Vite Configuration
- Displaying Race Results
- Horse Betting Is Complicated
- Handling Bet Simulation
- A chart
- Bonus Scraper
- Summary
Background
Over this last summer, I interned at eProcess Development. They build and maintain software for a variety of clients. I worked on several projects while there, but this one was particularly interesting.
The owner of ePD has, for the last few years, been accumulating a large quantity of data on horse racing, a favorite hobby of his. He came up with a betting strategy and designed a website. He outsourced the webdev to a group in India, who, over time, implemented most of the features he wanted at a low cost.
The website was very janky and had broken. He asked me to look at it and see what I could do.
Horror
The entirety of the business logic of the front end was contained in a 10,000+ line render
function of a React class component. This file was so massive that it broke my intellisense completely.
On the PHP back end, there were major security oversights.
The group in India had not been using any sort of version control, and it turned out we did not even have the current source code. They seemed unwilling or unable to send a current version.
I drafted a design document and proposed that I rewrite Betacus from scratch rather than futilely pull on the Gordion knot.
Setting Up
The server was already running PHP, and there were additional PHP data analysis tools hosted there (that were solid). Additionally, the data was already in MySQL tables. So I got to learn PHP!
I set up my local environment with docker containers; a PHP Apache container to simulate the server and a MySQL container to simulate the database. I poked around the running server to ensure my versions were as close as possible.
I used vite to transpile the TypeScript I would write to JavaScript. Vite provides a development server, so I set up my local PHP container to allow CORs when local in order to enjoy the hot-module-reloading of Vite.
I used a Makefile to orchestrate the environment. I have found Makefiles very helpful for managing docker containers - you can specify more granular commands than a simple docker compose can provide. And they work just find on windows - just install it with chocolatey!
Chakra
I wanted a good looking UI but, despite this beautiful blog, I don’t consider myself an expert designer. Chakra has prestyled components that were drop-in ready. Like, check out this popover.
An SVG
As I brainstormed this project, I got artsy. I rotoscoped a photo and came up with a nice SVG.
I set it up in 5 colors, and those colors can be changed programatically.
Vite Configuration
I configured Vite to output Betacus as a multi-page app. We don’t need to serve the larger JS files and images until someone needs them. This was also handy for development, as I built a variety of fixtures as I iterated. I was able to retrieve them easily without bogging down my browser.
I used a plugin, called virtual-mpa for this.
Displaying Race Results
The first iteration of Betacus involved getting clever with Gridboxes as I tried to use my horse SVG for every horse in every race. This was doubly tricky, as I really wanted to make this responsive enough for mobile.
I ultimately scrapped this in favor of abacus-looking beads.
Horse Betting Is Complicated
This was without a doubt the most challenging part about making Betacus.
Users are able to simulate bets for a day of historical race data. They can post multiple bets of a variety of types, and their cumulative winning or losings and ROI are presented. The issue is that there are some very complex types of bets in horse racing.
First you have your standard bets, such as Win, Place, or Show. These are simple enough, though a user needs to be able to select multiple horses for one win bet. For example, in one click through, I need to be able to place 3 $2.00 win bets that horse 1, 2, and 5 will ‘win’ for a total of cost of $6.00.
Next you have your horizontal bets. These are bets across multiple races. For example, you can pick the winner of race 2, 3, and 4 in a “Pick3” bet. Once again, you need to be able to select multiple horses. So if I pick 3 horses from race 1, 1 from race 2, and 1 from race 2, and the Pick3 is a $1 bet, the total cost is $3.00. However, if I pick 2 from race 3, then I have 6 combinations, and my total cost is $6.00.
Finally, you have vertical bets. For example, in the “Trifecta” bet, you bet that you know who will come in 1st, 2nd, and 3rd place exactly. So if I pick horse 1 for win, horse 2 for place, and horse 3 for show, it’s a $0.50 bet. Once again, multiple selections are allowed. So if I also add horse 4 to show, it’s now a $1.00 bet. What if I also add horse 4 to win? (So I am betting, 1 or 4 to win, 2 to show, 3 or 4 to place) That’s two combinations, because 4 can’t win and place! So it’s a $1.50 for that bet.
Yea, calculating these combinations was quite tricky and was one place I used unit tests extensively.
On top of that, horses sometimes run as “couples”. When you see a “1” and a “1A” in a race, those horses are paired. A bet on one is a bet on both, so they must both ‘select’ when clicked, and count as one for combinatorics purposes.
Handling Bet Simulation
I struggled with this problem for a while. I eventually solved it by using what I called a Context-Provider-Reducer pattern.
There were three CPR pattern groups; ‘display’, ‘racebet’, and ‘trackbet’. They communicate with each other so that a racebet sends messages to trackbet and a trackbet sends messages to display. There is a racebet for each race, a trackbet for each track, and a single display for the page.
A react reducer manages changes in state.
A react context exposes the dispatch function of the reducer, and exposes the state for reading.
A provider component provides the context and reducer to child components.
I also used a “Manager” class and extended it for each bet type.
I defined various user actions as types. For example:
type AllHorses_Clicked = {
name: 'select-all-clicked'
race: 0 | 1 | 2 | 3 // whether in 'win', 'place', or 'show' section
}
That would be processed in the reducer like so.
if (action.name == 'select-all-clicked') {
state.manager.clickSelectAll(action.race)
state.manager.updateBet()
return state.manager.getState()
}
If the state doesn’t match the previous state with Object.Is
, a re-render is triggered.
The manager’s clickSelectAll
function looked like this
clickSelectAll(race: 0 | 1 | 2 | 3) {
if (this.checkHelper.allChecked()) {
this.checkHelper.uncheckAll()
} else {
this.checkHelper.checkAll()
}
this.state.selections[0] = this.checkHelper.checked
}
The checkHelper was used to ensure couples were both selected when one was selected.
updateBet was the hackiest part of this whole code.
/** Dispatches a horizontal or vertical bet update to the Trackbet reducer. */
async updateBet() {
setTimeout(() => {
// this setTimeout is a hacky workaround for a react limitation. React can't update outer state while in the process of updating inner state. updateBet sends messages to the trackBet system, causing it to possibly update, but that's only allowed after this function returns.
this.dispatcher(this.getBetUpdate())
}, 100)
}
The provider component provides this racebet state to its children.
return <RacebetContext.Provider value=>
{children}
</RacebetContext.Provider>
It was used in the ‘Race’ component like so.
return <RacebetProvider data={data}>
<AbacusRow header={header}>
<HorsesRows data={data} />
</AbacusRow>
</RacebetProvider>
Custom hooks were added to let components access these providers.
export default function useRacebetHorseClick(horse:TRaceDay.Horse, race: 0 | 1 | 2 | 3 = 0) {
const rbet = useContext(RacebetContext)
return function () {
rbet.racebetDo({
name: 'horse-clicked',
race: race,
horse: horse,
})
}
}
And the end components utilized such hooks when needed.
I tried to make this code as maintainable as possible. I generated a lot of documentation, and this graph:
A chart
I added an extra feature, a chart. I made it with d3. I thought users might appreciate greater ability to analyze data in a given race.
The chart rescales itself. It also normalizes all data by taking the standard deviation.
Bonus Scraper
The client did not have much automation in their data gathering pipeline. They did most of it by hand themselves and outsourced a little to India. They had some clever excel worksheets for producing more fields once the base data was in, but I knew I could program them something to massively reduce the time they spent reading in data from websites. I scouted the internet for a simple site that presented much of the data we needed and built a tool that scraped that site.
I called it the betacus-gatherer
. On my first scrape, I copied all the pages I wanted to a local docker container, and scraped that during development, so as to not over-scrape the site and risk being blocked.
I wrote it in Go, because I wanted to be able to hand off a very simple .exe file that would just work. I used a local server for the front end rather than deal with complicated system user interfaces like QT or something. So when it starts up, you navigate to localhost in browser and configure your scrape from there.
One of the many neat things about Go is that you can embed files into your compiled binary, like so:
//go:embed templates/*
var templates embed.FS
//go:embed public
var public embed.FS
I used a library called colly for the scraping. I displayed the scraped data in an HTML table and added a button for copying the table so it could be pasted directly into Excel. Since I had analyzed some of the excel files used in the data entry process, I lined it up so it would be fully compatible.
In the end, I produced a 20 mb binary that the client could use right out of the box. They were thrilled. This wasn’t something I was asked for, but I recognized that a few hours of my work would save the client literally weeks of their life in data entry, and they now use it first whenever they start another data entry task.
Summary
This was an intense, complicated project. There were some very difficult front end problems to solve. I am not sure everyone could have done it, especially the bet simulation. I feel as though I wrote high quality, maintainable code, and I could jump back into this project tomorrow and add more features.
Throughout the process, I always kept the product I was making and the end user in mind. I made a good looking but simple interface for handling a large volume of complex data in a responsive way. I worked closely with my client (who also happened to be my boss) to ensure the product was what he was looking for, but also persuaded him to change his mind on certain features that I believed could be better. In other cases I dropped my ideas when they weren’t liked, conscious of the fact that this was their product and I was just the coder.
The client was extremely satisfied and has proposed revenue sharing if I develop Betacus further. I would like to get my hands on the data analysis part - I have some things I’d like to try to develop a betting strategy that generates positive ROI overall.
Thanks for reading!