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.

1. Load the packages

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.


2. Define the period and assets

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.


3. Download the data

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.


4. Inspect the data

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.


5. Plot the original prices

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.


6. Normalize prices

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.


7. Calculate daily returns

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.


8. Plot daily returns

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.


9. Compare return distributions

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.


10. Summary statistics

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.


11. Total return and annualized return

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

12. Annualized volatility

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.


13. Rolling volatility

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().


14. Sharpe ratio

Return alone is not sufficient to evaluate an investment.

The Sharpe ratio compares excess return with volatility:

\[ \text{Sharpe ratio} = \frac{R-R_f}{\sigma}. \]

Rather than assuming a risk-free rate of zero, we will download the 3-month U.S. Treasury constant-maturity yield from FRED (Federal Reserve Economic Data).

Its FRED code is:

DGS3MO

The series is expressed as an annual percentage yield.

Download it:

## NOT WORKING WHEN KNITED

risk_free <- tq_get(
  "DGS3MO",
  get = "economic.data",
  from = start_date,
  to = end_date
)

write_csv(risk_free, "data/risk_free.csv")
risk_free <- read_csv("data/risk_free.csv")
risk_free
## # A tibble: 2,554 × 3
##    symbol date       price
##    <chr>  <date>     <dbl>
##  1 DGS3MO 2017-01-03  0.53
##  2 DGS3MO 2017-01-04  0.53
##  3 DGS3MO 2017-01-05  0.52
##  4 DGS3MO 2017-01-06  0.53
##  5 DGS3MO 2017-01-09  0.5 
##  6 DGS3MO 2017-01-10  0.52
##  7 DGS3MO 2017-01-11  0.52
##  8 DGS3MO 2017-01-12  0.52
##  9 DGS3MO 2017-01-13  0.53
## 10 DGS3MO 2017-01-16 NA   
## # ℹ 2,544 more rows

Rename the variable:

risk_free <- risk_free |>
  select(date, price) |>
  rename(rf_annual = price)

FRED does not publish a new rate every calendar day.

We therefore create a complete calendar and carry the most recent rate forward:

risk_free <- risk_free |>
  complete(
    date = seq.Date(
      start_date,
      end_date,
      by = "day"
    )
  ) |>
  arrange(date) |>
  fill(
    rf_annual,
    .direction = "downup"
  )

fill() uses the latest available Treasury rate for dates on which no new observation was published.

The FRED value is expressed as an annual percentage rate, so we convert it to a daily rate:

risk_free <- risk_free |>
  mutate(
    rf_daily =
      (1 + rf_annual / 100)^(1 / 252) - 1
  )

Now combine returns with the risk-free rate:

returns_rf <- returns |>
  filter(asset %in% c("Bitcoin", "Gold")) |>
  left_join(select(risk_free, date, rf_daily), join_by(date)) |>
  mutate(excess_return = return - rf_daily)

The daily excess return is:

asset return − risk-free return

Finally calculate the annualized Sharpe ratio:

sharpe <- returns_rf |>
  group_by(asset) |>
  summarise(
    mean_daily_excess =
      mean(excess_return, na.rm = TRUE),

    sd_daily_excess =
      sd(excess_return, na.rm = TRUE),

    sharpe =
      mean_daily_excess /
      sd_daily_excess *
      sqrt(252)
  )

sharpe
## # A tibble: 2 × 4
##   asset   mean_daily_excess sd_daily_excess sharpe
##   <chr>               <dbl>           <dbl>  <dbl>
## 1 Bitcoin          0.00259           0.0421  0.975
## 2 Gold             0.000483          0.0107  0.716

The Sharpe ratio rewards higher excess return but penalizes volatility.

A higher Sharpe ratio indicates more excess return per unit of volatility.


15. Create a final comparison table

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.


16. Correlation between Bitcoin, Gold and the Dollar

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:

  1. Spurious correlation due to trends. Two unrelated assets may both increase in price over time, producing a high positive correlation between their price levels even if their daily movements are independent.
  2. Returns measure relative changes. A price increase of 10 has a very different meaning for an asset worth 20 than for one worth 1000. Returns normalize these changes, making assets with different price levels comparable.
  3. Returns better capture co-movement and diversification. In portfolio analysis, we want to know whether assets tend to gain or lose value together over the same periods. Correlations between returns provide a more meaningful measure of this relationship.

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.



17. Bitcoin and Gold versus 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.


18. Has correlation changed over time?

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.


19. Yearly returns

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.


20. What should we conclude from the analysis?

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”.


21. Complete workflow

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.