Crypto Currency (BTC) Prices - Are There Tradeable Daily Patterns?

Crypto Currency (BTC) Prices - Are There Tradeable Daily Patterns?

Intro

I’ve come across discussion on day-of-week patterns in Bitcoin in various circles, with the most common suggestion being that Bitcoin tends to be higher during the weekdays than on weekend days. Just because somebody says so, doesn’t make it so today, even if it may have been true in the past.

So do any patterns really exist? And, more importantly: if they do exist, are there profitable trades to be made on a reliable basis?

tl;dr

Surprise…no. At least as far as I can tell. ;)

Read on for the details! Of course, there may be flaws in my analysis. Use at your own risk - this is merely for infotainment purposes and is not at all intended as any sort of financial or trading advice. :)

Get Data

  • Focus on recent years, due to long-term volatility and evolution of the market focus.
  • Looking at Bitcoin as the apex crypto currency. Other coins may have entirely different patterns.
  • Using BTC-CAD because…well, I’m Canadian.
## get data
symb <- c('BTC-CAD')
date_st <- '2020-05-01'
## using auto.assign = FALSE and setting object name to avoid issues with default 'BTC-CAD' name
BTC_CAD <- getSymbols(Symbols=symb, from=date_st, to=Sys.Date()-1, auto.assign = FALSE)

Initial Look at Data

dygraph(BTC_CAD[,"BTC-CAD.Close"])


Focus on Recent Trading Range


Add Days of Week

  • Add weekdays to identify and compare prices by day of week.
  • Add date of each week to identify and compare weeks.

Easiest - for me, at least - to convert time series to data frame:

btc_rec_df <- data.frame(btc_rec)
btc_rec_df$date <- index(btc_rec)
## add days
btc_rec_df$day <- weekdays(index(btc_rec), abbreviate = TRUE)
## - set days to factors
btc_rec_df$day <- factor(btc_rec_df$day)
btc_rec_df$day <- fct_relevel(btc_rec_df$day, levels=c("Sun","Mon","Tue","Wed","Thu","Fri","Sat"))
## add weeks
btc_rec_df$week_of <- floor_date(btc_rec_df$date, unit='weeks')

View the structure of the data with additional components added:

## 'data.frame':    513 obs. of  9 variables:
##  $ BTC.CAD.Open    : num  36909 37396 40901 41748 40872 ...
##  $ BTC.CAD.High    : num  37682 42207 44057 42468 43642 ...
##  $ BTC.CAD.Low     : num  36667 37033 40803 36400 38539 ...
##  $ BTC.CAD.Close   : num  37393 40898 41711 40865 43090 ...
##  $ BTC.CAD.Volume  : num  51849388517 86393019962 100092071966 103739501829 85625353050 ...
##  $ BTC.CAD.Adjusted: num  37393 40898 41711 40865 43090 ...
##  $ date            : Date, format: "2021-01-01" "2021-01-02" ...
##  $ day             : Factor w/ 7 levels "Sun","Mon","Tue",..: 6 7 1 2 3 4 5 6 7 1 ...
##  $ week_of         : Date, format: "2020-12-27" "2020-12-27" ...

Comparative views

Take a look at daily comparisons for this period:

btc_rec_df %>% ggplot(aes(x=day, y=BTC.CAD.Close))+geom_boxplot(fill=fill_color)+
  scale_y_continuous(labels=dollar_format())+
  labs(title="Distribution of BTC-CAD Closing Price by Day of Week", y='BTC-CAD Close', x="")

No strong, obvious pattern over the period. Slight pattern of lower mid-week, rising to weekends.

  • Maybe Fri-Sat? Sun-Tue?

This data is over a period where there is a lot of variance in the price that may blur the daily patterns. Could still be that there is a consistent pattern within weeks.

For more granularity, let’s look at week-by-week trends by day:

week_plot <- btc_rec_df %>% ggplot(aes(x=day, y=BTC.CAD.Close, color=as.factor(week_of), group=week_of))+geom_line()+
  scale_y_continuous(labels=dollar_format())+
  scale_x_discrete(expand=c(0,0))+
  labs(title="BTC-CAD Closing Price by Day by Week",x="",y="BTC-CAD Close")+
  theme(legend.position = 'none')
ggplotly(week_plot)

Certainly some weeks with upward trend through the week (most obviously near end of 2021), but not exactly a consistent pattern to rely on across this date range.

Specific Day of Weeks Comparisons

Look at some individual day of week comparisons

Fri - Sat

btc_rec_d_df <- btc_rec_df %>% filter(day=='Fri' | day=='Sat')
dd_plot <- btc_rec_d_df %>% ggplot(aes(x=day, y=BTC.CAD.Close, color=as.factor(week_of), group=week_of))+geom_line()+
  scale_y_continuous(labels=dollar_format())+
  scale_x_discrete(expand=c(0,0))+
  labs(title="BTC-CAD Closing Price by Day by Week",x="",y="BTC-CAD Close")+
  theme(legend.position = 'none')
ggplotly(dd_plot)

Pretty hard to pick out any obvious/consistent pattern. Let’s take a closer look:

  • get each week % change from Wed to Sat.
  • get summary stats on these changes.
  • look at distribution of % change to see if any consistency.
## Wed - Sat
## may need to remove first row if starts with the day later in the week
#btc_rec_d_df <- btc_rec_d_df[-1,]

## may need to remove last row, if ends on day earlier in the week
#btc_rec_d_df <- btc_rec_d_df[-nrow(btc_rec_d_df),]
## calc % chg Wed-Sat
btc_rec_d_df <- btc_rec_d_df %>% mutate(
  wk_chg=BTC.CAD.Close/lag(BTC.CAD.Close)-1
)
## calculate some stats and make them pretty for printing
mwkchg_calc <- median(btc_rec_d_df$wk_chg, na.rm=TRUE)
mwkchg <- glue(prettyNum(mwkchg_calc*100, digits=2),"%")
awkchg_calc <- mean(btc_rec_d_df$wk_chg, na.rm=TRUE)
awkchg <- glue(prettyNum(awkchg_calc*100, digits=2),"%")
wkchg_pctl <- quantile(btc_rec_d_df$wk_chg, 0.5, na.rm=TRUE)
## set color for mean based on above/below zero
acolor <- ifelse(awkchg_calc>0,'green','red')
apos <- ifelse(awkchg_calc>0,0.03,-0.03)
mpos <- ifelse(mwkchg_calc>0,0.04,-0.04)
## histogram
btc_rec_d_df %>% ggplot(aes(x=wk_chg))+geom_histogram(fill=fill_color)+
  geom_vline(xintercept=mwkchg_calc, color='black', linetype='dashed', size=1)+
  geom_vline(xintercept=awkchg_calc, color=acolor, linetype='dashed', size=1)+
  annotate(geom='text', label=paste0("median: ",mwkchg), x=mwkchg_calc+mpos, y=16.4, color='black')+
  annotate(geom='text', label=paste0("ave: ",awkchg), x=awkchg_calc+apos, y=17.1, color=acolor)+
  labs(title="Distribution of Weekly Returns from Fri to Sat")

Basically a wash:

  • median of -0.03% tells us there is 50% chance of being either above or below -0.03% (very close to 0) return on the week, although farther outliers on the positive side.
  • Ave. return on the week could add up to something over time. (0.23%).

Sun - Tue

  • median of 0.45% tells us there is 50% chance of being either above or below 0.45%.
  • Ave. return on the week could add up to something over time. (0.2%).

Appears slightly more promising than Fri - Sat, considering both median and avg returns are above 0. Theorectically, these returns could add up to mad stacks of cash over time.

Two potential (major) issues:

  1. Transaction fees, spread, etc. will likely erase this small gains - at least at the level many of us operate.
  2. The gains may not be statistically significant enough to rely on.

We can assume that issue #1 is a deal-breaker, but let’s check issue #2 for fun.

For this we can use paired sample t-test. This is used to compare ‘before / after’ situations within the same samples.

  1. Calculate difference for each sample.
  2. Check for normal distribution of differences - esp. if sample <30.
  3. Run t-test with paired = TRUE.

Paired t-test

  • differences have already been calculated.
  • normality check: can be eye-balled in histogram above. For extra measure, check QQ plot and Shapiro-Wilk test. (Not really needed, since well over 30 samples, but what the heck)

## 
##  Shapiro-Wilk normality test
## 
## data:  btc_rec_d_df$wk_chg
## W = 1, p-value = 0.2

Looks reasonable! High p-value for Shapiro Wilk indicates NOT significantly different from normal.

So now the question is…is there a reliable difference?

Boxplot doesn’t look encouraging. Let’s confirm with hypothesis test.

  • run paired t.test:
t.test(data=btc_rec_d_df, BTC.CAD.Close ~ day, paired = TRUE)
## 
##  Paired t-test
## 
## data:  BTC.CAD.Close by day
## t = 0.1, df = 72, p-value = 0.9
## alternative hypothesis: true difference in means is not equal to 0
## 95 percent confidence interval:
##  -788  886
## sample estimates:
## mean of the differences 
##                    49.3

Looks like we do not have statical significance at all. So if this info was to be used for investment advice - which it definitely is not - the advice would be: do not try this at home. ;(

APPENDX: Full end-to-end code for reference

## get data
symb <- c('BTC-CAD')
date_st <- '2020-05-01'
## using auto.assign = FALSE and setting object name to avoid issues with default 'BTC-CAD' name
BTC_CAD <- getSymbols(Symbols=symb, from=date_st, to=Sys.Date()-1, auto.assign = FALSE)
dygraph(BTC_CAD[,"BTC-CAD.Close"])
rec <- '2021-01-01/'
btc_rec <- BTC_CAD[rec]
dygraph(btc_rec[,"BTC-CAD.Close"])
btc_rec_df <- data.frame(btc_rec)
btc_rec_df$date <- index(btc_rec)
## add days
btc_rec_df$day <- weekdays(index(btc_rec), abbreviate = TRUE)
## - set days to factors
btc_rec_df$day <- factor(btc_rec_df$day)
btc_rec_df$day <- fct_relevel(btc_rec_df$day, levels=c("Sun","Mon","Tue","Wed","Thu","Fri","Sat"))
## add weeks
btc_rec_df$week_of <- floor_date(btc_rec_df$date, unit='weeks')
btc_rec_df %>% ggplot(aes(x=day, y=BTC.CAD.Close))+geom_boxplot(fill=fill_color)+
  scale_y_continuous(labels=dollar_format())+
  labs(title="Distribution of BTC-CAD Closing Price by Day of Week", y='BTC-CAD Close', x="")
week_plot <- btc_rec_df %>% ggplot(aes(x=day, y=BTC.CAD.Close, color=as.factor(week_of), group=week_of))+geom_line()+
  scale_y_continuous(labels=dollar_format())+
  scale_x_discrete(expand=c(0,0))+
  labs(title="BTC-CAD Closing Price by Day by Week",x="",y="BTC-CAD Close")+
  theme(legend.position = 'none')
ggplotly(week_plot)
btc_rec_d_df <- btc_rec_df %>% filter(day=='Fri' | day=='Sat')
dd_plot <- btc_rec_d_df %>% ggplot(aes(x=day, y=BTC.CAD.Close, color=as.factor(week_of), group=week_of))+geom_line()+
  scale_y_continuous(labels=dollar_format())+
  scale_x_discrete(expand=c(0,0))+
  labs(title="BTC-CAD Closing Price by Day by Week",x="",y="BTC-CAD Close")+
  theme(legend.position = 'none')
ggplotly(dd_plot)
## Wed - Sat
## may need to remove first row if starts with the day later in the week
#btc_rec_d_df <- btc_rec_d_df[-1,]

## may need to remove last row, if ends on day earlier in the week
#btc_rec_d_df <- btc_rec_d_df[-nrow(btc_rec_d_df),]
## calc % chg Wed-Sat
btc_rec_d_df <- btc_rec_d_df %>% mutate(
  wk_chg=BTC.CAD.Close/lag(BTC.CAD.Close)-1
)
## calculate some stats and make them pretty for printing
mwkchg_calc <- median(btc_rec_d_df$wk_chg, na.rm=TRUE)
mwkchg <- glue(prettyNum(mwkchg_calc*100, digits=2),"%")
awkchg_calc <- mean(btc_rec_d_df$wk_chg, na.rm=TRUE)
awkchg <- glue(prettyNum(awkchg_calc*100, digits=2),"%")
wkchg_pctl <- quantile(btc_rec_d_df$wk_chg, 0.5, na.rm=TRUE)
## set color for mean based on above/below zero
acolor <- ifelse(awkchg_calc>0,'green','red')
apos <- ifelse(awkchg_calc>0,0.03,-0.03)
mpos <- ifelse(mwkchg_calc>0,0.04,-0.04)
## histogram
btc_rec_d_df %>% ggplot(aes(x=wk_chg))+geom_histogram(fill=fill_color)+
  geom_vline(xintercept=mwkchg_calc, color='black', linetype='dashed', size=1)+
  geom_vline(xintercept=awkchg_calc, color=acolor, linetype='dashed', size=1)+
  annotate(geom='text', label=paste0("median: ",mwkchg), x=mwkchg_calc+mpos, y=16.4, color='black')+
  annotate(geom='text', label=paste0("ave: ",awkchg), x=awkchg_calc+apos, y=17.1, color=acolor)+
  labs(title="Distribution of Weekly Returns from Fri to Sat")
  
## Sun - Tue
btc_rec_d_df <- btc_rec_df %>% filter(day=='Sun' | day=='Tue')
## may need to remove last row, if ends on day earlier in the week
#btc_rec_d_df <- btc_rec_d_df[-nrow(btc_rec_d_df),]
dd_plot <- btc_rec_d_df %>% ggplot(aes(x=day, y=BTC.CAD.Close, color=as.factor(week_of), group=week_of))+geom_line()+
  scale_y_continuous(labels=dollar_format())+
  scale_x_discrete(expand=c(0,0))+
  labs(title="BTC-CAD Closing Price by Day by Week",x="",y="BTC-CAD Close")+
  theme(legend.position = 'none')
ggplotly(dd_plot)
## calc % chg Sun-Tue
btc_rec_d_df <- btc_rec_d_df %>% mutate(
  wk_chg=BTC.CAD.Close/lag(BTC.CAD.Close)-1
)
## calculate some stats and make them pretty for printing
mwkchg_calc <- median(btc_rec_d_df$wk_chg, na.rm=TRUE)
mwkchg <- glue(prettyNum(mwkchg_calc*100, digits=2),"%")
awkchg_calc <- mean(btc_rec_d_df$wk_chg, na.rm=TRUE)
awkchg <- glue(prettyNum(awkchg_calc*100, digits=2),"%")
wkchg_pctl <- quantile(btc_rec_d_df$wk_chg, 0.5, na.rm=TRUE)
## set color for mean based on above/below zero
acolor <- ifelse(awkchg_calc>0,'green','red')
apos <- ifelse(awkchg_calc>0,0.03,-0.03)
mpos <- ifelse(mwkchg_calc>0,0.04,-0.04)
## histogram
btc_rec_d_df %>% ggplot(aes(x=wk_chg))+geom_histogram(fill=fill_color)+
  geom_vline(xintercept=mwkchg_calc, color='black', linetype='dashed', size=1)+
  geom_vline(xintercept=awkchg_calc, color=acolor, linetype='dashed', size=1)+
  annotate(geom='text', label=paste0("median: ",mwkchg), x=mwkchg_calc+mpos, y=16.4, color='black')+
  annotate(geom='text', label=paste0("ave: ",awkchg), x=awkchg_calc+apos, y=17.1, color=acolor)+
  labs(title="Distribution of Weekly Returns from Sun to Tue")
## check normality
qqnorm(btc_rec_d_df$wk_chg) ## qqplot
qqline(btc_rec_d_df$wk_chg) ## shows line of perfect normal
shapiro.test(btc_rec_d_df$wk_chg) ## Shapiro-Wilk test for normality
## check boxplot
btc_rec_d_df %>% ggplot(aes(x=day, y=BTC.CAD.Close))+geom_boxplot(fill=fill_color)
t.test(data=btc_rec_d_df, BTC.CAD.Close ~ day, paired = TRUE)