Photo by Omar Ram on Unsplash

Using Technical Analysis Techniques in Football

A momentum-based approach to visualizing team strength in the English Premier League

Morten Andersen
CodeX
Published in
7 min readSep 21, 2021

--

Simple Moving Average (SMA) indicators are a simple, yet powerful tool for momentum-based trading strategies. I’m neither for nor against the use of technical analysis, momentum-based strategies, or the like, in finance as almost every trading strategies have their use-cases if the trader is skilled enough. But stepping outside of its usual application, I figured it would be cool to try and apply it to English Premier League (EPL) football (soccer for the American readers).

A (Very) Brief Introduction to Momentum Trading

Disclaimer: This very brief introduction is by no means financial advice. Please do not use it as such. You should not enter any financial markets without doing your own research first. Investing can be risky and you may lose what you invest.

Momentum trading is generally a short-term trading strategy in which a trader tries to buy stocks that is on the rise and sell stocks when they have peaked. There are many ways of determining which stocks seem to be on the rise and which have peaked. One method is the simple moving average crossover technique, in which a trader calculates two different moving averages from the stock’s time-series data. One of them should be a ‘longer’ moving average, meaning that it should use more data in the calculations, than the other, e.g. MA10 and MA50. The basic idea is then to buy when the shorter moving average crosses above the longer moving average and vice versa.[1]
Let us try to apply this idea to football.

EPL Football Application

Setting aside the whole betting perspective, obviously, we can’t buy a team that is on the rise, or similar, sell when it has peaked. We can, however, try to use the moving average crossover technique to get a sense of the current form/strength of the team. There are several ways to do this. In the example presented here, I will only visualize four different ‘metrics’. One is a team’s points per game and the three others are based on scores, namely, a team’s scored goals, goals scored against the team, and the goal difference, i.e. goals scored minus goals against. I will discuss other metrics that can be used later in the article. Furthermore, I have chosen to present this visualization by creating a Shiny app in R to give the analysis a more dynamic feel.[2, 3, 4, 5]
Unfortunately, the Shiny app is not currently hosted anywhere, but the repo is publically available on my Github.

I’ll interpret a team as being on the rise when the shorter moving average crosses over the longer ones, and vice versa, just like a regular stock, but my endpoint is not to buy or sell but rather to visualize team form patterns. Obviously, choosing different lengths of the moving averages will make the results vary greatly, but this is, in fact, also the very thing that makes momentum techniques, especially the crossover strategies, more of an art form than a simple formula to make you rich.

The Data

I obtained the football data directly from football-data.co.uk using the following R code.[6]

EPL <- read_csv("https://www.football-data.co.uk/mmz4281/2122/E0.csv") %>%
bind_rows(read_csv("https://www.football-data.co.uk/mmz4281/2021/E0.csv")) %>%
bind_rows(read_csv("https://www.football-data.co.uk/mmz4281/2021/E1.csv")) %>%
bind_rows(read_csv("https://www.football-data.co.uk/mmz4281/1920/E0.csv")) %>%
bind_rows(read_csv("https://www.football-data.co.uk/mmz4281/1920/E1.csv")) %>%
bind_rows(read_csv("https://www.football-data.co.uk/mmz4281/1819/E0.csv")) %>%
bind_rows(read_csv("https://www.football-data.co.uk/mmz4281/1819/E1.csv")) %>%
bind_rows(read_csv("https://www.football-data.co.uk/mmz4281/1718/E0.csv")) %>%
bind_rows(read_csv("https://www.football-data.co.uk/mmz4281/1718/E1.csv")) %>%
mutate(
Date = dmy(Date),
HomePoint = case_when(
FTR == "H" ~ 3L,
FTR == "D" ~ 1L,
FTR == "A" ~ 0L
),
AwayPoint = case_when(
FTR == "A" ~ 3L,
FTR == "D" ~ 1L,
FTR == "H" ~ 0L
)
) %>%
arrange(Date, Time) %>%
select(Div, Date, HomeTeam, AwayTeam,
FTHG, FTAG, HomePoint, AwayPoint)
teams <- EPL %>%
filter(Div == "E0") %>%
distinct(HomeTeam) %>%
pull() %>%
sort()

The R code above generates a dataset of historic EPL matches, their results, and the distribution of match points between them. I have included English Championship matches as well in order to have a full dataset for newly promoted teams in the EPL.

Screen capture of the R console showing the EPL dataset (by author)

R Functions

First, let me present the first of two main R functions used in the Shiny app: get_team_ma_scores. This function uses the EPL dataset and calculates a team’s moving averages.

get_team_ma_scores <- function(df, team, ma = c(10L, 50L)) {
#' Get Team Moving Average Scores
#'
#' Function to filter out a given teams' scores and calculate
#' moving averages of these scores.
#'
#' @param df Dataframe containing the EPL scores data.
#' @param team string. The name of the team.
#' @param ma numeric. The length of the two moving averages.
#'
#' @author Morten Andersen

first_ma_slide <-
timetk::slidify(mean, .period = ma[1], .align = "right")
second_ma_slide <-
timetk::slidify(mean, .period = ma[2], .align = "right")

ll <- list()
ll[[paste0("MA", ma[1])]] <- first_ma_slide
ll[[paste0("MA", ma[2])]] <- second_ma_slide

df %>%
filter(
str_detect(HomeTeam, pattern = team) |
str_detect(AwayTeam, pattern = team)
) %>%
mutate(
Goal = if_else(HomeTeam == team, FTHG, FTAG),
Goal_against = if_else(HomeTeam == team, FTAG, FTHG),
Goal_dif = Goal - Goal_against,
Point = if_else(HomeTeam == team, HomePoint, AwayPoint)
) %>%
select(-FTHG, -FTAG, -HomePoint, -AwayPoint) %>%
mutate(across(starts_with(c("Goal", "Point")), .fns = ll))
}

Next, we need to visualize the moving averages. This is done with the second R function create_crossover_plot.

create_crossover_plot <- function(df, metric, team) {
#' Create Moving Averages Crossover Plot
#'
#' Function to prepare the data based on selections and
#' then create the moving averages crossover plot.
#'
#' @param df Dataframe containing a team's moving average scores.
#' @param metric string. The type of metric to show.
#' @param team string. The name of the team.
#'
#' @author Morten Andersen

p <- na.omit(df) %>%
select(Date, starts_with(metric)) %>%
pivot_longer(cols = starts_with(paste0(metric, "_MA")),
names_to = "type",
values_to = "Value") %>%
ggplot(aes(x = Date, y = Value, col = type)) +
ggtitle(paste(team, metric, sep = " - ")) +
geom_path() +
theme_classic()

plotly::ggplotly(p)
}

Now, using these two functions with the inputs team = 'Arsenal' and
metric = 'Point' generates (as of 2021–09–17) the following moving average crossover plot for Arsenal.

A crossover plot of Arsenal’s point moving averages (by author)

Looking at the crossover plot of Arsenal’s point-moving averages, we would conclude that Arsenal is currently in a decent period, that started with them being on the rise, i.e. crossing over, in late spring of 2021. This means that Arsenal is currently getting more points per game than their long-term (moving) average indicates. However, looking at a different metric with the same moving averages lengths, we get a different conclusion. Shown below is Arsenal’s goal difference moving average crossover plot, which shows that Arsenal has recently crossed below their long-term goal difference average, i.e. they are currently ending games with a lower goal difference than usual. Looking at the broader picture, we do see the same general patterns from the two different metrics, though, but since goal difference and points have a very high correlation it is not surprising. This little case study shows the importance of trying out different metrics and moving average lengths, and of being a little skeptical of the interpretation of the data.

A crossover plot of Arsenal’s goal difference moving averages (by author)

Shiny App

Let us piece it all together in a Shiny app to get some dynamic, interactive analysis across multiple teams going.
First, the UI part:

ui <- function(){
navbarPage(
title = "English Premier League",
theme = shinytheme("slate"),
tabPanel(
title = "Moving Average Crossover",
wellPanel(
fluidRow(
useSever(),
column(
width = 3,
selectInput(
"team",
label = "Select a team",
choices = teams,
selected = teams[1]
)
),
column(
width = 3,
selectInput(
"metric",
label = "Select a metric",
choices = c("Point","Goal","Goal_against","Goal_dif"),
selected = "Point"
)
),
column(
width = 3,
numericInput(
"ma1",
label = "Length of moving average #1",
value = 10L,
min = 2L,
max = 30L
)
),
column(
width = 3,
numericInput(
"ma2",
label = "Length of moving average #2",
value = 50L,
min = 10L,
max = 100L
)
)
)
),
plotlyOutput("crossover_plot") %>% withSpinner()
)
)
}

Next up, the server logic:

server <- function(input, output, session){ 
team_df <- reactive({
get_team_ma_scores(
df = EPL,
team = input$team,
ma = c(input$ma1, input$ma2)
)
})
output$crossover_plot <- renderPlotly(
create_crossover_plot(
df = team_df(),
metric = input$metric,
team = input$team
)
)
sever(opacity = 0.85)
}

Finally, this yields the following interactive English Premier League - Moving Average Crossover Shiny app.

Screen capture of the Shiny app (by author)

Full project-based code for the Shiny app can be seen here: https://github.com/MoAnd/EPL_MA_Crossover

Extensions

As mentioned earlier, only imagination (and data capture abilities) sets the limits for which metrics to represent with the moving average crossover technique. As an example, we may look at team or individual player scores from e.g. SofaScore or FootballCritic. Other football-related stats could be of interest as well, e.g. expected goals (xGoals), shots, shots-on-target, etc. Furthermore, it can sometimes be beneficial to examine rolling medians instead of the means, especially if you have few observations that seriously affect the mean (outliers).

References

[1] Adam Barone, “Introduction to Momentum Trading” (Accessed Sept. 2021), Investopedia, https://www.investopedia.com/trading/introduction-to-momentum-trading/.

[2] R Core Team, “R: A language and environment for statistical computing” (2021), R Foundation for Statistical Computing, Vienna, Austria. URL https://www.R-project.org/.

[3] Wickham et al., “Welcome to the tidyverse” (2019), Journal of Open Source Software, 4(43), 1686.

[4] Winston Chang, Joe Cheng, JJ Allaire, Carson Sievert, Barret Schloerke, Yihui Xie, Jeff Allen, Jonathan McPherson, Alan Dipert, and Barbara Borges, shiny: Web Application Framework for R (2021), R package version 1.6.0. https://CRAN.R-project.org/package=shiny

[5] C. Sievert, Interactive Web-Based Data Visualization with R, plotly, and shiny (2020), Chapman and Hall/CRC Florida.

[6] Football-Data.co.uk (Accessed Sept. 2021), https://www.football-data.co.uk/englandm.php.

--

--

Morten Andersen
CodeX
Writer for

Quantitative Trader | R Programmer | MSc in Mathematics-Economics