In this data challenge we want to compare Bitcoin and gold as financial assets from January 2017 until the current date. We investigate:
We will use:
BTC-USD Bitcoin in U.S. dollars
GC=F COMEX gold futures
DX-Y.NYB U.S. Dollar Index
An important qualification is that GC=F represents gold
futures rather than physical spot gold. It is nevertheless a convenient
long historical gold series available through Yahoo Finance. If the
objective is to study an investable security instead, GLD
could be substituted as a gold ETF proxy.
library(tidyquant)
library(tidyverse)
tidyquant provides access to financial and economic data
and integrates many quantitative-finance functions with tidy data.
The tidyverse provides the tools for data manipulation and visualization.
start_date <- as.Date("2017-01-01")
end_date <- Sys.Date()
assets <- tibble(
symbol = c("BTC-USD", "GC=F", "DX-Y.NYB"),
asset = c("Bitcoin", "Gold", "Dollar")
)
assets
## # A tibble: 3 × 2
## symbol asset
## <chr> <chr>
## 1 BTC-USD Bitcoin
## 2 GC=F Gold
## 3 DX-Y.NYB Dollar
start_date defines the beginning of the analysis.
Using
Sys.Date()
makes the analysis automatically extend to the day on which the script is executed.
The assets tibble associates Yahoo Finance ticker
symbols with more readable names.
prices <- tq_get(
assets$symbol,
from = start_date,
to = end_date
) |>
left_join(assets, join_by(symbol))
write_csv(prices, "data/prices.csv")
prices <- read_csv("data/prices.csv")
prices
## # A tibble: 8,980 × 9
## symbol date open high low close volume adjusted asset
## <chr> <date> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <chr>
## 1 BTC-USD 2017-01-01 964. 1003. 959. 998. 147775008 998. Bitcoin
## 2 BTC-USD 2017-01-02 999. 1031. 997. 1022. 222184992 1022. Bitcoin
## 3 BTC-USD 2017-01-03 1022. 1044. 1022. 1044. 185168000 1044. Bitcoin
## 4 BTC-USD 2017-01-04 1044. 1159. 1044. 1155. 344945984 1155. Bitcoin
## 5 BTC-USD 2017-01-05 1157. 1191. 910. 1013. 510199008 1013. Bitcoin
## 6 BTC-USD 2017-01-06 1014. 1047. 884. 902. 351876000 902. Bitcoin
## 7 BTC-USD 2017-01-07 903. 909. 824. 909. 279550016 909. Bitcoin
## 8 BTC-USD 2017-01-08 908. 943. 887. 911. 158715008 911. Bitcoin
## 9 BTC-USD 2017-01-09 913. 914. 880. 903. 141876992 903. Bitcoin
## 10 BTC-USD 2017-01-10 902. 915. 901. 908. 115808000 908. Bitcoin
## # ℹ 8,970 more rows
tq_get() downloads historical prices from Yahoo Finance.
By default it retrieves variables such as:
date
open
high
low
close
volume
adjusted
left_join() adds the more readable names
Bitcoin, Gold, and Dollar.
We will use the closing price:
prices <- prices |>
select(asset, symbol, date, close)
For these assets, using close gives us a simple common
definition of the daily market price.
Before performing an analysis, always inspect the dataset.
prices |>
group_by(asset) |>
summarise(
first_date = min(date),
last_date = max(date),
observations = n()
)
## # A tibble: 3 × 4
## asset first_date last_date observations
## <chr> <date> <date> <int>
## 1 Bitcoin 2017-01-01 2026-10-02 3562
## 2 Dollar 2017-01-03 2026-10-01 2963
## 3 Gold 2017-01-03 2026-10-01 2455
This tells us:
We should expect Bitcoin to contain more observations because Bitcoin trades seven days per week.
Gold and the Dollar Index trade primarily on business days.
This difference is important.
The three series have very different scales, so a faceted plot is appropriate:
prices |>
ggplot(aes(x = date, y = close)) +
geom_line() +
facet_wrap(
~ asset,
scales = "free_y",
ncol = 1
) +
labs(
title = "Bitcoin, Gold and the U.S. Dollar Index",
subtitle = "January 2017 to today",
x = NULL,
y = NULL
)
facet_wrap() creates one plot for each asset.
The argument
scales = "free_y"
allows every asset to have its own vertical scale.
Without it, the enormous difference between Bitcoin and the other series would make the graph difficult to interpret.
Raw prices cannot be directly compared.
One bitcoin, one ounce of gold, and one unit of the Dollar Index have completely different units.
A better comparison is to set every asset equal to 100 at the beginning.
First, we align the prices on dates available for all three assets:
common_prices <- prices |>
select(date, asset, close) |>
pivot_wider(
names_from = asset,
values_from = close
) |>
drop_na()
pivot_wider() creates one column for each asset.
drop_na() keeps only dates on which Bitcoin, gold and
the Dollar Index all have observations.
This creates a common trading calendar.
Now return to long format:
common_prices_long <- common_prices |>
pivot_longer(
cols = c(Bitcoin, Gold, Dollar),
names_to = "asset",
values_to = "price"
)
We can normalize each price series:
indexed_prices <- common_prices_long |>
group_by(asset) |>
arrange(date) |>
mutate(
index = 100 * price / first(price)
) |>
ungroup()
For every asset,
100 * price / first(price)
sets the first observation equal to 100.
For example:
100 no change
150 gain of 50%
80 loss of 20%
Now plot Bitcoin and gold:
indexed_prices |>
filter(asset %in% c("Bitcoin", "Gold")) |>
ggplot(aes(x = date, y = index, color = asset)) +
geom_line() +
labs(
title = "Bitcoin versus Gold",
subtitle = "Initial value = 100",
x = NULL,
y = "Index",
color = NULL
)
This graph compares investment growth rather than nominal prices.
Prices are useful for showing long-run evolution, but financial comparisons are normally based on returns.
For each asset we calculate:
\[ r_t = \frac{P_t}{P_{t-1}} - 1 \]
In R:
returns <- common_prices_long |>
group_by(asset) |>
arrange(date) |>
mutate(
return = price / lag(price) - 1
) |>
filter(!is.na(return)) |>
ungroup()
returns
## # A tibble: 7,347 × 4
## date asset price return
## <date> <chr> <dbl> <dbl>
## 1 2017-01-04 Bitcoin 1155. 0.106
## 2 2017-01-04 Gold 1164. 0.00293
## 3 2017-01-04 Dollar 103. -0.00494
## 4 2017-01-05 Bitcoin 1013. -0.122
## 5 2017-01-05 Gold 1180. 0.0137
## 6 2017-01-05 Dollar 102. -0.0115
## 7 2017-01-06 Bitcoin 902. -0.110
## 8 2017-01-06 Gold 1172. -0.00661
## 9 2017-01-06 Dollar 102. 0.00690
## 10 2017-01-09 Bitcoin 903. 0.000695
## # ℹ 7,337 more rows
lag(price) represents the previous common trading day’s
price.
Thus:
price / lag(price) - 1
calculates the percentage price change.
For example:
0.02 +2%
-0.03 -3%
Notice that we aligned prices before calculating returns.
This is deliberate. Bitcoin trades during weekends while gold and the Dollar Index generally do not. By first retaining common dates, returns for the three assets refer to comparable intervals.
returns |>
filter(asset %in% c("Bitcoin", "Gold")) |>
ggplot(aes(x = date, y = return)) +
geom_line() +
facet_wrap(
~ asset,
ncol = 1
) +
labs(
title = "Daily Returns",
subtitle = "Bitcoin and Gold",
x = NULL,
y = "Return"
)
This plot immediately reveals an important characteristic that is much less obvious from the price chart: volatility.
Large positive and negative movements appear as large deviations from zero.
Bitcoin will generally display much larger fluctuations than gold.
Another useful view is the distribution of returns:
returns |>
filter(asset %in% c("Bitcoin", "Gold")) |>
ggplot(aes(x = return)) +
geom_histogram(
bins = 80
) +
facet_wrap(
~ asset,
scales = "free_y"
) +
labs(
title = "Distribution of Daily Returns",
x = "Daily return",
y = "Frequency"
)
The histogram allows us to compare:
Bitcoin’s distribution should be considerably wider if its daily volatility is larger.
We can calculate several basic return statistics:
return_summary <- returns |>
filter(asset %in% c("Bitcoin", "Gold")) |>
group_by(asset) |>
summarise(
mean_daily = mean(return),
median_daily = median(return),
sd_daily = sd(return),
min_daily = min(return),
max_daily = max(return)
)
return_summary
## # A tibble: 2 × 6
## asset mean_daily median_daily sd_daily min_daily max_daily
## <chr> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 Bitcoin 0.00269 0.00137 0.0421 -0.372 0.252
## 2 Gold 0.000583 0.000724 0.0107 -0.108 0.0607
The statistics have the following meanings:
mean_daily average daily return
median_daily median daily return
sd_daily standard deviation of daily returns
min_daily worst daily return
max_daily best daily return
The standard deviation is our basic measure of volatility.
To compare long-term performance we can calculate total return:
performance <- common_prices_long |>
filter(asset %in% c("Bitcoin", "Gold")) |>
group_by(asset) |>
arrange(date) |>
summarise(
start_date = first(date),
end_date = last(date),
start_price = first(price),
end_price = last(price),
total_return = end_price / start_price - 1
)
performance
## # A tibble: 2 × 6
## asset start_date end_date start_price end_price total_return
## <chr> <date> <date> <dbl> <dbl> <dbl>
## 1 Bitcoin 2017-01-03 2026-10-01 1044. 84853. 80.3
## 2 Gold 2017-01-03 2026-10-01 1160. 4202. 2.62
For example, a total_return of:
4
means a return of 400%.
It is often useful to express long-term growth as a compound annual growth rate, or CAGR.
The compound annual growth rate (CAGR) measures the average annual growth rate of an investment or quantity over a given period, assuming that growth is compounded each year.
It is calculated as:
\[ \mathrm{CAGR}=\left(\frac{V_f}{V_i}\right)^{1/n}-1 \]
where:
For example, if an investment grows from 100 to 150 over 5 years, its CAGR is:
\[ \mathrm{CAGR}=\left(\frac{150}{100}\right)^{1/5}-1\approx 8.45\\% \]
This means that a constant annual growth rate of 8.45%, compounded over five years, would produce the same overall increase.
performance <- common_prices_long |>
filter(asset %in% c("Bitcoin", "Gold")) |>
group_by(asset) |>
arrange(date) |>
summarise(
start_price = first(price),
end_price = last(price),
years = as.numeric(last(date) - first(date)) / 365.25,
total_return = end_price / start_price - 1,
CAGR =
(end_price / start_price)^(1 / years) - 1
)
performance
## # A tibble: 2 × 6
## asset start_price end_price years total_return CAGR
## <chr> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 Bitcoin 1044. 84853. 9.74 80.3 0.571
## 2 Gold 1160. 4202. 9.74 2.62 0.141
Daily volatility is difficult to interpret directly, so it is conventional to annualize it.
For approximately 252 trading days per year:
\[ \sigma_{\text{annual}} = \sigma_{\text{daily}}\sqrt{252}. \]
In R:
volatility <- returns |>
filter(asset %in% c("Bitcoin", "Gold")) |>
group_by(asset) |>
summarise(
daily_volatility = sd(return),
annual_volatility = sd(return) * sqrt(252)
)
volatility
## # A tibble: 2 × 3
## asset daily_volatility annual_volatility
## <chr> <dbl> <dbl>
## 1 Bitcoin 0.0421 0.669
## 2 Gold 0.0107 0.170
If annual volatility is:
0.20
that corresponds to approximately 20%.
The calculation assumes daily returns are sufficiently independent for the square-root-of-time approximation.
A single volatility number hides how risk changes over time.
We can use tq_mutate() to compute a 30-observation
rolling standard deviation:
rolling_volatility <- returns |>
filter(asset %in% c("Bitcoin", "Gold")) |>
group_by(asset) |>
tq_mutate(
select = return,
mutate_fun = runSD,
n = 30,
col_rename = "rolling_sd"
) |>
mutate(
rolling_volatility = rolling_sd * sqrt(252)
) |>
ungroup()
runSD() calculates a rolling standard deviation.
The argument
n = 30
means that each volatility estimate uses the previous 30 common trading observations.
We annualize it by multiplying by:
sqrt(252)
Now plot it:
rolling_volatility |>
ggplot(
aes(
x = date,
y = rolling_volatility,
color = asset
)
) +
geom_line() +
labs(
title = "30-Day Rolling Annualized Volatility",
x = NULL,
y = "Annualized volatility",
color = NULL
)
## Warning: Removed 58 rows containing missing values or values outside the scale range
## (`geom_line()`).
This plot shows whether the difference between Bitcoin and gold risk is stable over time or concentrated in particular periods.
tidyquant is specifically designed to apply quantitative
functions such as these through tq_mutate() and
tq_transmute().
We can combine our main measures:
comparison <- common_prices_long |>
filter(asset %in% c("Bitcoin", "Gold")) |>
group_by(asset) |>
arrange(date) |>
summarise(
start_price = first(price),
end_price = last(price),
years = as.numeric(last(date) - first(date)) / 365.25,
total_return = end_price / start_price - 1,
CAGR = (end_price / start_price)^(1 / years) - 1) |>
left_join(select(volatility, asset, annual_volatility), join_by(asset)) |>
left_join(select(sharpe, asset, sharpe), join_by(asset))
comparison
## # A tibble: 2 × 8
## asset start_price end_price years total_return CAGR annual_volatility sharpe
## <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 Bitco… 1044. 84853. 9.74 80.3 0.571 0.669 0.975
## 2 Gold 1160. 4202. 9.74 2.62 0.141 0.170 0.716
This gives one compact table containing:
asset
start price
end price
total return
compound annual growth rate
annualized volatility
Sharpe ratio
For easier reading:
comparison |>
mutate(
total_return =
scales::percent(total_return, accuracy = 0.1),
CAGR =
scales::percent(CAGR, accuracy = 0.1),
annual_volatility =
scales::percent(
annual_volatility,
accuracy = 0.1
),
sharpe =
round(sharpe, 2)
)
## # A tibble: 2 × 8
## asset start_price end_price years total_return CAGR annual_volatility sharpe
## <chr> <dbl> <dbl> <dbl> <chr> <chr> <chr> <dbl>
## 1 Bitco… 1044. 84853. 9.74 8 028.9% 57.1% 66.9% 0.98
## 2 Gold 1160. 4202. 9.74 262.1% 14.1% 17.0% 0.72
The percentages are formatted for presentation rather than calculation.
Correlation should be calculated using returns, not raw price levels.
Correlations between trending price series can be misleading because two prices can trend over time even without a meaningful short-run relationship. There are three main reasons:
First reshape returns:
returns_wide <- returns |>
select(date, asset, return) |>
pivot_wider(
names_from = asset,
values_from = return
) |>
drop_na()
returns_wide
## # A tibble: 2,449 × 4
## date Bitcoin Gold Dollar
## <date> <dbl> <dbl> <dbl>
## 1 2017-01-04 0.106 0.00293 -0.00494
## 2 2017-01-05 -0.122 0.0137 -0.0115
## 3 2017-01-06 -0.110 -0.00661 0.00690
## 4 2017-01-09 0.000695 0.00990 -0.00284
## 5 2017-01-10 0.00537 0.000591 0.000785
## 6 2017-01-11 -0.143 0.00963 -0.00225
## 7 2017-01-12 0.0348 0.00276 -0.00422
## 8 2017-01-13 0.0238 -0.00300 -0.00168
## 9 2017-01-17 0.102 0.0140 -0.00840
## 10 2017-01-18 -0.0235 -0.000578 0.00598
## # ℹ 2,439 more rows
We now have:
date
Bitcoin
Gold
Dollar
where each asset column contains its daily return.
Calculate the Pearson correlation matrix:
cor_matrix <- returns_wide |>
select(Bitcoin, Gold, Dollar) |>
cor()
cor_matrix
## Bitcoin Gold Dollar
## Bitcoin 1.00000000 0.09851251 -0.0976418
## Gold 0.09851251 1.00000000 -0.3904170
## Dollar -0.09764180 -0.39041704 1.0000000
The possible values range from:
-1 perfect negative correlation
0 no linear correlation
+1 perfect positive correlation
The correlations of particular interest are:
cor(
returns_wide$Bitcoin,
returns_wide$Gold
)
## [1] 0.09851251
cor(
returns_wide$Bitcoin,
returns_wide$Dollar
)
## [1] -0.0976418
cor(
returns_wide$Gold,
returns_wide$Dollar
)
## [1] -0.390417
The first measures whether Bitcoin and gold tend to move together.
The second measures the relationship between Bitcoin and the U.S. dollar.
The third measures the relationship between gold and the dollar.
We can compare both assets with the Dollar Index in a single graph.
First reshape the data:
dollar_relationship <- returns_wide |>
pivot_longer(
cols = c(Bitcoin, Gold),
names_to = "asset",
values_to = "asset_return"
)
Now plot:
dollar_relationship |>
ggplot(
aes(
x = Dollar,
y = asset_return
)
) +
geom_point(
alpha = 0.3
) +
geom_smooth(
method = "lm",
se = FALSE
) +
facet_wrap(
~ asset,
scales = "free_y"
) +
labs(
title = "Bitcoin and Gold versus the U.S. Dollar",
x = "Dollar Index return",
y = "Asset return"
)
This makes it easy to compare the two relationships visually.
A negative correlation with the Dollar Index means that the asset tends, on average, to rise when the dollar weakens and fall when the dollar strengthens.
A correlation close to zero implies little contemporaneous linear relationship.
This does not establish a causal effect of the dollar on the asset.
A single correlation for 2017–today may conceal substantial changes.
A simple way to investigate this is to calculate correlations separately by year:
yearly_correlations <- returns_wide |>
mutate(
year = format(date, "%Y")
) |>
group_by(year) |>
summarise(
bitcoin_gold =
cor(Bitcoin, Gold),
bitcoin_dollar =
cor(Bitcoin, Dollar),
gold_dollar =
cor(Gold, Dollar)
)
yearly_correlations
## # A tibble: 10 × 4
## year bitcoin_gold bitcoin_dollar gold_dollar
## <chr> <dbl> <dbl> <dbl>
## 1 2017 -0.0180 0.0725 -0.456
## 2 2018 0.0124 -0.0462 -0.592
## 3 2019 0.215 -0.135 -0.431
## 4 2020 0.274 -0.0700 -0.288
## 5 2021 -0.0519 -0.114 -0.433
## 6 2022 0.0936 -0.316 -0.472
## 7 2023 0.114 -0.138 -0.500
## 8 2024 0.0629 -0.0283 -0.306
## 9 2025 0.0976 -0.0294 -0.405
## 10 2026 0.239 -0.205 -0.407
format(date, "%Y") extracts the calendar year.
The three cor() calls then calculate the correlations
separately within each year.
Convert to long format:
yearly_correlations_long <-
yearly_correlations |>
pivot_longer(
-year,
names_to = "pair",
values_to = "correlation"
)
And visualize them:
yearly_correlations_long |>
ggplot(
aes(
x = year,
y = correlation,
color = pair,
group = pair
)
) +
geom_hline(
yintercept = 0,
linetype = "dashed"
) +
geom_line() +
geom_point() +
labs(
title = "Correlation through Time",
x = NULL,
y = "Correlation",
color = NULL
) +
theme(
axis.text.x =
element_text(
angle = 45,
hjust = 1
)
)
This is often more informative than reporting only one full-period correlation.
Bitcoin’s relationship with gold or the dollar need not remain constant through time.
We can also compare annual performance.
yearly_returns <- returns |>
filter(
asset %in%
c("Bitcoin", "Gold")
) |>
mutate(
year = format(date, "%Y")
) |>
group_by(asset, year) |>
summarise(
annual_return =
prod(1 + return) - 1,
.groups = "drop"
)
yearly_returns
## # A tibble: 20 × 3
## asset year annual_return
## <chr> <chr> <dbl>
## 1 Bitcoin 2017 13.0
## 2 Bitcoin 2018 -0.745
## 3 Bitcoin 2019 0.922
## 4 Bitcoin 2020 3.03
## 5 Bitcoin 2021 0.597
## 6 Bitcoin 2022 -0.641
## 7 Bitcoin 2023 1.54
## 8 Bitcoin 2024 1.22
## 9 Bitcoin 2025 -0.0634
## 10 Bitcoin 2026 -0.0303
## 11 Gold 2017 0.126
## 12 Gold 2018 -0.0214
## 13 Gold 2019 0.189
## 14 Gold 2020 0.247
## 15 Gold 2021 -0.0351
## 16 Gold 2022 -0.00131
## 17 Gold 2023 0.134
## 18 Gold 2024 0.275
## 19 Gold 2025 0.644
## 20 Gold 2026 -0.0320
For each year,
prod(1 + return) - 1
compounds all daily returns.
Plot the results:
yearly_returns |>
ggplot(
aes(
x = year,
y = annual_return,
fill = asset
)
) +
geom_col(
position = "dodge"
) +
scale_y_continuous(
labels = scales::percent
) +
labs(
title = "Annual Returns: Bitcoin versus Gold",
x = NULL,
y = "Annual return",
fill = NULL
) +
theme(
axis.text.x =
element_text(
angle = 45,
hjust = 1
)
)
This chart prevents the overall 2017–today result from obscuring very different individual years.
The analysis should distinguish several different questions.
Price performance asks:
Which asset appreciated more?
The indexed-price graph, total return, and CAGR answer this question.
Risk asks:
How variable were the returns?
Daily return distributions, annualized volatility, and rolling volatility address this question.
Risk-adjusted return asks:
How much excess return was obtained for the volatility experienced?
The Sharpe ratio addresses this question.
Diversification asks:
Do the assets tend to move together?
The Bitcoin–gold return correlation provides evidence about this.
The relationship with the dollar asks:
Do Bitcoin and gold tend to rise or fall when the U.S. dollar strengthens?
The correlations with the Dollar Index address this question.
These questions should not be collapsed into a single statement about which asset is “better”.
The complete empirical workflow is:
tq_get()
↓
download Bitcoin, gold and dollar data
↓
align common trading dates
↓
normalize prices
↓
calculate returns
↓
measure performance
↓
measure volatility
↓
calculate Sharpe ratios
↓
calculate correlations
↓
examine changes through time
↓
interpret the different dimensions together
The most important methodological principle is that price comparisons, return comparisons, volatility comparisons, and correlation analysis answer different questions.
In particular, correlations should generally be calculated using returns rather than raw prices, and assets with different trading calendars should be aligned before cross-asset return analysis.