R at Bet Better › R for Betting-Market Data
Chapter 5. How prices move before kick-off
20 October 2026. Edward Glush. Code run in R 4.4.1 against the live open data on that day.
A price is not fixed. It opens, then moves as money and news come in, and closes just before the start. This chapter loads an open file of opening and closing prices and asks where the moves are biggest.
Data and licence. Dataset: Bet Better price movement dataset 2026 on Zenodo (DOI 10.5281/zenodo.23048677), 6,882 rows, CC BY 4.0. Prices are averaged across every book tracked; no bookmaker is named. The codebook is in the record. The first walk-through is this post.
5.1 Load it
u <- "https://zenodo.org/records/23048677/files/betbetter-price-movement-2026.csv?download=1"
mv <- read.csv(u)
dim(mv)
names(mv)
## [1] 6882 14
## [1] "sport" "game_date"
## [3] "home_team" "away_team"
## [5] "market" "selection"
## [7] "line" "opening_avg_price"
## [9] "closing_avg_price" "price_move_pct"
## [11] "n_books_open" "n_books_close"
## [13] "closing_fair_probability" "hours_tracked"
Each row is one outcome in one settled market. The key columns:
opening_avg_price and closing_avg_price: the average decimal price when the market was first seen and just before the start.
price_move_pct: the change from open to close, in per cent of the opening price. Positive means the price got longer (the outcome looked less likely).
n_books_open, n_books_close: how many books went into each average.
closing_fair_probability: the closing probability with the margin removed.
hours_tracked: hours between the first sighting and the closing snapshot.
table(mv$market)
length(unique(mv$sport))
##
## h2h spreads totals
## 1926 2264 2692
## [1] 22
5.2 The size of a typical move
summary(mv$price_move_pct)
mean(abs(mv$price_move_pct))
mean(mv$price_move_pct > 0)
mean(mv$price_move_pct < 0)
mean(mv$price_move_pct == 0)
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## -34.3700 -1.7375 0.1100 0.8851 2.2600 100.3500
## [1] 3.993346
## [1] 0.5193258
## [1] 0.4707934
## [1] 0.009880849
The middle move is close to zero, but the average size of a move, ignoring direction, is about 4 per cent. A few prices doubled. Prices got longer a little more often than shorter. Under 1 per cent of rows did not move at all.
Use the absolute move when you ask how much prices move. The signed average mixes ups and downs, and they cancel out.
5.3 By sport
The sport column holds a competition key such as tennis_wta_us_open. The first word is the sport. We use sub() to keep it.
mv$sport_group <- sub("_.*", "", mv$sport)
by_sport <- aggregate(abs(price_move_pct) ~ sport_group, data = mv,
FUN = function(x) c(n = length(x), mean = mean(x), median = median(x)))
by_sport <- do.call(data.frame, by_sport)
names(by_sport) <- c("sport", "rows", "mean_abs_move", "median_abs_move")
by_sport[, 3:4] <- round(by_sport[, 3:4], 2)
by_sport[order(-by_sport$mean_abs_move), ]
## sport rows mean_abs_move median_abs_move
## 5 cricket 80 13.20 6.20
## 8 soccer 1847 6.67 4.47
## 6 mma 196 6.32 4.50
## 9 tennis 408 3.29 1.47
## 3 baseball 1562 3.16 2.42
## 4 basketball 362 2.81 0.96
## 7 rugbyleague 71 2.78 1.50
## 1 americanfootball 2326 2.28 1.00
## 2 aussierules 30 1.99 0.32
Cricket, soccer and MMA move the most. American football and Australian rules move the least. Some groups are small (30 rows for Australian rules, 71 for rugby league), so their figures are rough. The median sits well below the mean for most sports, which says a few large moves pull the average up.
5.4 By price
Is a long shot's price more jumpy than a favourite's? Put each row in a band by its opening probability.
mv$p_open <- 1 / mv$opening_avg_price
mv$band <- cut(mv$p_open, c(0, .2, .4, .6, .8, 1), include.lowest = TRUE)
table(mv$band)
round(tapply(abs(mv$price_move_pct), mv$band, mean), 2)
round(tapply(mv$price_move_pct, mv$band, mean), 2)
##
## [0,0.2] (0.2,0.4] (0.4,0.6] (0.6,0.8] (0.8,1]
## 189 764 5097 656 176
## [0,0.2] (0.2,0.4] (0.4,0.6] (0.6,0.8] (0.8,1]
## 16.62 7.70 3.09 3.62 2.00
## [0,0.2] (0.2,0.4] (0.4,0.6] (0.6,0.8] (0.8,1]
## 8.22 2.77 0.45 0.16 0.18
Long shots move far more in per cent terms, about 16.6 per cent on average under a 20 per cent chance. Part of that is arithmetic: a move from 8.00 to 9.00 is 12.5 per cent, while the probability changed by under two points. The signed average shows something more: long shots drifted out (got longer) on average, while prices near even barely moved on balance.
To compare like with like, measure the move in probability points instead:
mv$p_close <- 1 / mv$closing_avg_price
round(tapply(abs(mv$p_close - mv$p_open) * 100, mv$band, mean), 2)
## [0,0.2] (0.2,0.4] (0.4,0.6] (0.6,0.8] (0.8,1]
## 1.82 2.19 1.55 2.39 1.74
Now the picture changes. In probability points, every band moves by a similar amount: about 1.5 to 2.4 points on average. So the huge per-cent moves on long shots are mostly arithmetic. A long price has to move a long way in per cent to shift its chance by one point. This is worth remembering whenever you read a headline about a price "crashing" or "drifting": ask what the move was in points of probability.
Which measure should you use? It depends on the question. If you care about the money a bettor gets back, the per-cent move in the price is the right one. If you care about what the market now believes, the move in probability is the honest one.
5.5 By time open
summary(mv$hours_tracked)
mv$tracked_band <- cut(mv$hours_tracked, c(-Inf, 12, 24, 48, 96, Inf))
table(mv$tracked_band)
round(tapply(abs(mv$price_move_pct), mv$tracked_band, mean), 2)
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 3.0 36.0 131.0 171.8 195.0 1547.0
##
## (-Inf,12] (12,24] (24,48] (48,96] (96, Inf]
## 134 504 1922 456 3866
## (-Inf,12] (12,24] (24,48] (48,96] (96, Inf]
## 0.84 2.07 3.34 3.10 4.78
Half the markets were tracked for more than 131 hours, about five and a half days. A long tail runs past two months; those are mostly futures-style markets and early-listed fixtures.
Markets seen for under 12 hours moved less than 1 per cent on average. Markets open for more than four days moved almost 5 per cent. More time means more news and more money, so this is what you would expect. It also means a raw comparison of sports is partly a comparison of how early each sport's markets open.
5.6 Open, close and the fair close
The file gives the closing fair probability. We can ask how far each raw price sat from it.
mv$p_close <- 1 / mv$closing_avg_price
round(mean(abs(mv$p_open - mv$closing_fair_probability)), 4)
round(mean(abs(mv$p_close - mv$closing_fair_probability)), 4)
round(cor(mv$p_open, mv$closing_fair_probability), 4)
## [1] 0.0307
## [1] 0.025
## [1] 0.9789
The closing raw price sits on average 2.5 points from the fair close. That gap is just the margin, since both come from the same prices. The opening price sits 3.1 points away: the margin plus the move. The open and the fair close are still very close overall, with a correlation of 0.98. Most of the information is in the price from the start.
To learn whether the close is a better forecast than the open, you need results. This file has none. The method is chapter 4: score both prices against what happened, and compare the Brier scores.
5.7 A histogram
hist(mv$price_move_pct[abs(mv$price_move_pct) < 30], breaks = 60,
main = "Price move from open to close", xlab = "Move (%)")
One caution before you read too much into any of this. These are average prices across many books. A single book can move more, or less, or at a different time. The average smooths out the noise of one book, which is helpful, but it also hides the moment when the first book moved and the others followed. The n_books_open and n_books_close columns tell you how many books went into each average. Rows priced by only a few books are noisier.
The plot shows a tall, narrow peak at zero with long tails both ways. It drops the few rows with moves above 30 per cent so the centre is easy to see.
Try it yourself
- Split the move by
market (h2h, spreads, totals). Do lines move as much as winners?
- Use
n_books_open to drop rows priced by fewer than five books. Does the average move shrink?
- Pair the two sides of each two-way market and check that their moves point in opposite directions.
Data licensed CC BY 4.0, free to reuse with a credit. Model estimates, not advice. 18+. If gambling is causing you harm: gamblinghelponline.org.au.