R Note 4 - Trading Strategy

Author
Affiliation

Asst. Prof. Calvin J. Chiou

National Chengchi University (NCCU)

Course Information

  • Course Name: Investments | 000351051

  • Semester: Spring 2026

Introduction

This document demonstrates several quantitative strategies applied to the Taiwan 50 ETF (symbol: 0050.TW), one of the most actively traded ETFs representing large-cap stocks in Taiwan. We cover four approaches:

  1. Moving Average (MA) Crossover — a trend-following strategy based on short- vs. long-term moving averages.
  2. RSI Strategy — a mean-reversion strategy using the Relative Strength Index.
  3. Bollinger Bands Strategy — a volatility-based mean-reversion strategy.
  4. Dollar-Cost Averaging (DCA) — a passive strategy that invests a fixed amount of NT$20,000 each month, regardless of market conditions.

All strategies start with the same initial capital of NT$1,000,000 over the period January 1, 2023 to January 1, 2026. A buy-and-hold 0050.TW position serves as the common benchmark throughout.


1. Retrieve Historical Price Data

We begin by retrieving daily adjusted price data for 0050.TW. This single dataset drives all four strategies.

etf_data <- tq_get("0050.TW",
                   from = "2023-01-01",
                   to   = "2026-01-01")
head(etf_data)
# A tibble: 6 × 8
  symbol  date        open  high   low close   volume adjusted
  <chr>   <date>     <dbl> <dbl> <dbl> <dbl>    <dbl>    <dbl>
1 0050.TW 2023-01-03  27.4  27.7  27.1  27.7 59242536     25.1
2 0050.TW 2023-01-04  27.6  27.7  27.5  27.6 55019412     25.0
3 0050.TW 2023-01-05  27.8  27.9  27.7  27.8 48283616     25.2
4 0050.TW 2023-01-06  27.8  28.0  27.8  28.0 46347120     25.3
5 0050.TW 2023-01-09  28.5  29.0  28.5  29.0 59992304     26.2
6 0050.TW 2023-01-10  29.0  29.1  28.9  29.1 47897080     26.3

2. Moving Average (MA) Crossover Strategy

The MA crossover is the baseline strategy. We use a short-term MA20 and a long-term MA50 to generate buy/sell signals.

2.1. Calculate Moving Averages

etf_ma <- etf_data |> 
  mutate(
    MA20 = SMA(adjusted, n = 20),
    MA50 = SMA(adjusted, n = 50)
  )
head(etf_ma)
# A tibble: 6 × 10
  symbol  date        open  high   low close   volume adjusted  MA20  MA50
  <chr>   <date>     <dbl> <dbl> <dbl> <dbl>    <dbl>    <dbl> <dbl> <dbl>
1 0050.TW 2023-01-03  27.4  27.7  27.1  27.7 59242536     25.1    NA    NA
2 0050.TW 2023-01-04  27.6  27.7  27.5  27.6 55019412     25.0    NA    NA
3 0050.TW 2023-01-05  27.8  27.9  27.7  27.8 48283616     25.2    NA    NA
4 0050.TW 2023-01-06  27.8  28.0  27.8  28.0 46347120     25.3    NA    NA
5 0050.TW 2023-01-09  28.5  29.0  28.5  29.0 59992304     26.2    NA    NA
6 0050.TW 2023-01-10  29.0  29.1  28.9  29.1 47897080     26.3    NA    NA

2.2. Generate Trading Signals

  • Buy when MA20 crosses above MA50 (golden cross)
  • Sell when MA20 crosses below MA50 (death cross)
etf_ma_signals <- etf_ma |> 
  mutate(
    signal = case_when(
      lag(MA20) < lag(MA50) & MA20 >= MA50 ~ "buy",
      lag(MA20) > lag(MA50) & MA20 <= MA50 ~ "sell",
      TRUE ~ NA_character_
    )
  )

etf_ma_signals |> 
  filter(!is.na(signal)) |> 
  select(date, adjusted, MA20, MA50, signal)
# A tibble: 12 × 5
   date       adjusted  MA20  MA50 signal
   <date>        <dbl> <dbl> <dbl> <chr> 
 1 2023-04-28     27.2  27.6  27.7 sell  
 2 2023-05-26     29.1  27.7  27.7 buy   
 3 2023-08-16     29.1  29.9  30.0 sell  
 4 2023-11-14     30.3  29.3  29.3 buy   
 5 2024-08-12     43.0  43.8  43.9 sell  
 6 2024-10-01     44.3  43.4  43.3 buy   
 7 2024-12-09     47.3  46.3  46.4 sell  
 8 2024-12-24     47.8  46.9  46.8 buy   
 9 2025-03-06     46.0  47.7  47.7 sell  
10 2025-05-21     44.9  42.6  42.3 buy   
11 2025-12-08     62.8  60.9  60.9 sell  
12 2025-12-23     62.8  61.8  61.7 buy   

2.3. Simulate the MA Strategy

capital  <- 1000000
position <- 0
cash     <- capital
portfolio_ma <- data.frame()

for (i in seq_len(nrow(etf_ma_signals))) {
  date_i  <- etf_ma_signals$date[i]
  price_i <- etf_ma_signals$adjusted[i]
  sig     <- etf_ma_signals$signal[i]

  if (!is.na(sig)) {
    if (sig == "buy") {
      position_new <- floor(cash / price_i)
      position <- position + position_new
      cash     <- cash - position_new * price_i
    } else if (sig == "sell") {
      cash     <- cash + position * price_i
      position <- 0
    }
  }
  total_value <- cash + position * price_i
  portfolio_ma <- bind_rows(portfolio_ma,
                   data.frame(date = date_i, price = price_i,
                              position, cash, total_value))
}

final_ma  <- tail(portfolio_ma$total_value, 1)
return_ma <- (final_ma - capital) / capital * 100
cat("MA Strategy — Final Value: NT$", round(final_ma, 0),
    "| Total Return:", round(return_ma, 2), "%\n")
MA Strategy — Final Value: NT$ 2101443 | Total Return: 110.14 %

2.4. Visualize MA Indicators and Signals

Code
# Mark buy/sell dates for flag annotation
buy_signals  <- etf_ma_signals |> filter(signal == "buy")
sell_signals <- etf_ma_signals |> filter(signal == "sell")

highchart(type = "stock") |>
  hc_title(text = "0050.TW — MA Crossover Signals") |>
  hc_add_series(etf_ma, type = "line", hcaes(x = date, y = adjusted),
                name = "0050 Price", color = "#234E70") |>
  hc_add_series(etf_ma, type = "line", hcaes(x = date, y = MA20),
                name = "MA20", color = "#E07A5F", dashStyle = "ShortDash") |>
  hc_add_series(etf_ma, type = "line", hcaes(x = date, y = MA50),
                name = "MA50", color = "#3D405B", dashStyle = "ShortDot") |>
  hc_add_series(buy_signals,  type = "scatter", hcaes(x = date, y = adjusted),
                name = "Buy",  color = "green",  marker = list(symbol = "triangle",   size = 8)) |>
  hc_add_series(sell_signals, type = "scatter", hcaes(x = date, y = adjusted),
                name = "Sell", color = "red",    marker = list(symbol = "triangle-down", size = 8)) |>
  hc_tooltip(valueDecimals = 2, shared = FALSE) |>
  hc_legend(enabled = T, align = "center", verticalAlign = "bottom") |>
  hc_add_theme(hc_theme_smpl())

3. RSI Strategy

The Relative Strength Index (RSI) measures the speed and change of recent price movements on a scale from 0 to 100. Unlike the MA crossover, which is a trend-following rule, RSI is a mean-reversion indicator: extreme readings suggest the asset is overbought or oversold and likely to reverse.

\[RSI = 100 - \frac{100}{1 + RS}, \quad RS = \frac{\text{Average Gain (14 days)}}{\text{Average Loss (14 days)}}\]

The standard thresholds are:

  • RSI < 30 → oversold → Buy
  • RSI > 70 → overbought → Sell

3.1. Compute RSI

etf_rsi <- etf_data |> 
  mutate(RSI = RSI(adjusted, n = 14))
head(etf_rsi)
# A tibble: 6 × 9
  symbol  date        open  high   low close   volume adjusted   RSI
  <chr>   <date>     <dbl> <dbl> <dbl> <dbl>    <dbl>    <dbl> <dbl>
1 0050.TW 2023-01-03  27.4  27.7  27.1  27.7 59242536     25.1    NA
2 0050.TW 2023-01-04  27.6  27.7  27.5  27.6 55019412     25.0    NA
3 0050.TW 2023-01-05  27.8  27.9  27.7  27.8 48283616     25.2    NA
4 0050.TW 2023-01-06  27.8  28.0  27.8  28.0 46347120     25.3    NA
5 0050.TW 2023-01-09  28.5  29.0  28.5  29.0 59992304     26.2    NA
6 0050.TW 2023-01-10  29.0  29.1  28.9  29.1 47897080     26.3    NA

3.2. Generate RSI Signals

We enter a position when RSI drops below 30 and exit when it rises above 70.

etf_rsi_signals <- etf_rsi |> 
  mutate(
    signal = case_when(
      lag(RSI) >= 30 & RSI < 30  ~ "buy",   # crosses into oversold
      lag(RSI) <= 70 & RSI > 70  ~ "sell",  # crosses into overbought
      TRUE ~ NA_character_
    )
  )

etf_rsi_signals |> 
  filter(!is.na(signal)) |> 
  select(date, adjusted, RSI, signal)
# A tibble: 30 × 4
   date       adjusted   RSI signal
   <date>        <dbl> <dbl> <chr> 
 1 2023-05-26     29.1  75.4 sell  
 2 2023-06-02     29.3  70.8 sell  
 3 2023-06-07     29.6  73.0 sell  
 4 2023-06-12     29.8  71.2 sell  
 5 2023-11-15     30.6  70.1 sell  
 6 2023-12-18     31.4  70.2 sell  
 7 2023-12-27     31.9  71.5 sell  
 8 2024-01-25     32.6  71.4 sell  
 9 2024-02-15     34.2  79.0 sell  
10 2024-03-21     37.8  75.2 sell  
# ℹ 20 more rows

3.3. Simulate the RSI Strategy

position <- 0
cash     <- capital
portfolio_rsi <- data.frame()

for (i in seq_len(nrow(etf_rsi_signals))) {
  date_i  <- etf_rsi_signals$date[i]
  price_i <- etf_rsi_signals$adjusted[i]
  sig     <- etf_rsi_signals$signal[i]

  if (!is.na(sig)) {
    if (sig == "buy") {
      position_new <- floor(cash / price_i)
      position <- position + position_new
      cash     <- cash - position_new * price_i
    } else if (sig == "sell") {
      cash     <- cash + position * price_i
      position <- 0
    }
  }
  total_value <- cash + position * price_i
  portfolio_rsi <- bind_rows(portfolio_rsi,
                    data.frame(date = date_i, price = price_i,
                               position, cash, total_value))
}

final_rsi  <- tail(portfolio_rsi$total_value, 1)
return_rsi <- (final_rsi - capital) / capital * 100
cat("RSI Strategy — Final Value: NT$", round(final_rsi, 0),
    "| Total Return:", round(return_rsi, 2), "%\n")
RSI Strategy — Final Value: NT$ 1304196 | Total Return: 30.42 %

3.4. Visualize RSI and Signals

Code
buy_rsi  <- etf_rsi_signals |> filter(signal == "buy")
sell_rsi <- etf_rsi_signals |> filter(signal == "sell")

# Panel 1: price with buy/sell markers
p1 <- highchart(type = "stock") |>
  hc_title(text = "0050.TW — RSI Strategy Signals") |>
  hc_add_series(etf_rsi, type = "line", hcaes(x = date, y = adjusted),
                name = "0050 Price", color = "#234E70") |>
  hc_add_series(buy_rsi,  type = "scatter", hcaes(x = date, y = adjusted),
                name = "Buy",  color = "green", marker = list(symbol = "triangle",      size = 8)) |>
  hc_add_series(sell_rsi, type = "scatter", hcaes(x = date, y = adjusted),
                name = "Sell", color = "red",   marker = list(symbol = "triangle-down", size = 8)) |>
  hc_tooltip(valueDecimals = 2) |>
  hc_legend(enabled = T, align = "center", verticalAlign = "bottom") |>
  hc_add_theme(hc_theme_smpl())

# Panel 2: RSI oscillator with threshold bands
p2 <- highchart() |>
  hc_title(text = "RSI(14) — Overbought / Oversold") |>
  hc_xAxis(type = "datetime") |>
  hc_yAxis(min = 0, max = 100,
           plotLines = list(
             list(value = 70, color = "red",   dashStyle = "ShortDash", width = 1,
                  label = list(text = "Overbought (70)", align = "right")),
             list(value = 30, color = "green", dashStyle = "ShortDash", width = 1,
                  label = list(text = "Oversold (30)",   align = "right"))
           )) |>
  hc_add_series(etf_rsi, type = "line", hcaes(x = date, y = RSI),
                name = "RSI(14)", color = "#8B5CF6") |>
  hc_tooltip(valueDecimals = 2) |>
  hc_add_theme(hc_theme_smpl())

p1
Code
p2

4. Bollinger Bands Strategy

Bollinger Bands place an envelope around a moving average using the rolling standard deviation of prices. When prices hit the lower band, the market is considered locally cheap (buy); when prices hit the upper band, it is considered locally expensive (sell).

\[\text{Upper Band} = MA_{20} + 2\,\sigma_{20}, \qquad \text{Lower Band} = MA_{20} - 2\,\sigma_{20}\]

where \(\sigma_{20}\) is the 20-day rolling standard deviation of the adjusted close.

4.1. Compute Bollinger Bands

bb <- BBands(etf_data$adjusted, n = 20, sd = 2)

etf_bb <- etf_data |> 
  mutate(
    BB_lower = bb[, "dn"],
    BB_mid   = bb[, "mavg"],
    BB_upper = bb[, "up"],
    pct_b    = bb[, "pctB"]   # percent-B: 0 = lower band, 1 = upper band
  )
head(etf_bb)
# A tibble: 6 × 12
  symbol  date        open  high   low close   volume adjusted BB_lower BB_mid
  <chr>   <date>     <dbl> <dbl> <dbl> <dbl>    <dbl>    <dbl>    <dbl>  <dbl>
1 0050.TW 2023-01-03  27.4  27.7  27.1  27.7 59242536     25.1       NA     NA
2 0050.TW 2023-01-04  27.6  27.7  27.5  27.6 55019412     25.0       NA     NA
3 0050.TW 2023-01-05  27.8  27.9  27.7  27.8 48283616     25.2       NA     NA
4 0050.TW 2023-01-06  27.8  28.0  27.8  28.0 46347120     25.3       NA     NA
5 0050.TW 2023-01-09  28.5  29.0  28.5  29.0 59992304     26.2       NA     NA
6 0050.TW 2023-01-10  29.0  29.1  28.9  29.1 47897080     26.3       NA     NA
# ℹ 2 more variables: BB_upper <dbl>, pct_b <dbl>

4.2. Generate Bollinger Band Signals

We buy when price crosses below the lower band and sell when price crosses above the upper band.

etf_bb_signals <- etf_bb |> 
  mutate(
    signal = case_when(
      lag(adjusted) >= lag(BB_lower) & adjusted < BB_lower ~ "buy",
      lag(adjusted) <= lag(BB_upper) & adjusted > BB_upper ~ "sell",
      TRUE ~ NA_character_
    )
  )

etf_bb_signals |> 
  filter(!is.na(signal)) |> 
  select(date, adjusted, BB_lower, BB_upper, signal)
# A tibble: 42 × 5
   date       adjusted BB_lower BB_upper signal
   <date>        <dbl>    <dbl>    <dbl> <chr> 
 1 2023-03-23     28.2     27.0     28.2 sell  
 2 2023-04-21     27.4     27.5     28.3 buy   
 3 2023-05-17     27.9     26.8     27.8 sell  
 4 2023-05-26     29.1     26.5     28.9 sell  
 5 2023-06-13     30.4     27.7     30.3 sell  
 6 2023-08-14     29.2     29.4     30.7 buy   
 7 2023-09-21     28.8     28.9     30.0 buy   
 8 2023-09-26     28.7     28.7     30.1 buy   
 9 2023-11-15     30.6     28.2     30.6 sell  
10 2023-12-14     31.3     30.5     31.1 sell  
# ℹ 32 more rows

4.3. Simulate the Bollinger Band Strategy

position <- 0
cash     <- capital
portfolio_bb <- data.frame()

for (i in seq_len(nrow(etf_bb_signals))) {
  date_i  <- etf_bb_signals$date[i]
  price_i <- etf_bb_signals$adjusted[i]
  sig     <- etf_bb_signals$signal[i]

  if (!is.na(sig)) {
    if (sig == "buy") {
      position_new <- floor(cash / price_i)
      position <- position + position_new
      cash     <- cash - position_new * price_i
    } else if (sig == "sell") {
      cash     <- cash + position * price_i
      position <- 0
    }
  }
  total_value <- cash + position * price_i
  portfolio_bb <- bind_rows(portfolio_bb,
                   data.frame(date = date_i, price = price_i,
                              position, cash, total_value))
}

final_bb  <- tail(portfolio_bb$total_value, 1)
return_bb <- (final_bb - capital) / capital * 100
cat("Bollinger Bands Strategy — Final Value: NT$", round(final_bb, 0),
    "| Total Return:", round(return_bb, 2), "%\n")
Bollinger Bands Strategy — Final Value: NT$ 1568357 | Total Return: 56.84 %

4.4. Visualize Bollinger Bands and Signals

Code
buy_bb  <- etf_bb_signals |> filter(signal == "buy")
sell_bb <- etf_bb_signals |> filter(signal == "sell")

highchart(type = "stock") |>
  hc_title(text = "0050.TW — Bollinger Bands Strategy") |>
  hc_add_series(etf_bb, type = "line", hcaes(x = date, y = adjusted),
                name = "0050 Price",   color = "#234E70") |>
  hc_add_series(etf_bb, type = "line", hcaes(x = date, y = BB_upper),
                name = "Upper Band",   color = "#E07A5F", dashStyle = "ShortDash") |>
  hc_add_series(etf_bb, type = "line", hcaes(x = date, y = BB_mid),
                name = "MA20 (Middle)", color = "#9CA3AF", dashStyle = "ShortDot") |>
  hc_add_series(etf_bb, type = "line", hcaes(x = date, y = BB_lower),
                name = "Lower Band",   color = "#10B981", dashStyle = "ShortDash") |>
  hc_add_series(buy_bb,  type = "scatter", hcaes(x = date, y = adjusted),
                name = "Buy",  color = "green", marker = list(symbol = "triangle",      size = 8)) |>
  hc_add_series(sell_bb, type = "scatter", hcaes(x = date, y = adjusted),
                name = "Sell", color = "red",   marker = list(symbol = "triangle-down", size = 8)) |>
  hc_tooltip(valueDecimals = 2, shared = FALSE) |>
  hc_legend(enabled = T, align = "center", verticalAlign = "bottom") |>
  hc_add_theme(hc_theme_smpl())

5. Dollar-Cost Averaging (DCA)

The three strategies above all require market-timing decisions. Dollar-Cost Averaging (DCA) abandons timing entirely. Instead, the investor commits a fixed amount of money — say NT$30,000 — on the first trading day of each month, regardless of whether the market is up or down.

The key insight is that the same fixed amount buys more shares when prices are low and fewer shares when prices are high, automatically creating a form of anti-cyclical investing.

5.1. Simulate Monthly DCA

We set aside the full NT$1,000,000 as the cash reserve from day one and deploy NT$30,000 on the first trading day of each month. Any undeployed cash earns no return (a conservative assumption that ignores money-market interest).

monthly_invest <- 30000       # NT$ invested each month
total_reserve  <- 1000000     # cash reserve (never more than this deployed in total)

# Identify the first trading day of each month
first_trading_days <- etf_data |>
  mutate(ym = format(date, "%Y-%m")) |>
  group_by(ym) |>
  slice_min(date, n = 1) |>
  ungroup() |>
  select(date) |>
  pull()

# Simulate DCA
cash_dca      <- total_reserve
shares_dca    <- 0
portfolio_dca <- data.frame()
total_invested <- 0   # track cumulative cash deployed

for (i in seq_len(nrow(etf_data))) {
  date_i  <- etf_data$date[i]
  price_i <- etf_data$adjusted[i]

  # On the first trading day of each month, buy as many whole shares as possible
  # with the monthly budget (if cash is still available)
  if (date_i %in% first_trading_days && cash_dca >= monthly_invest) {
    new_shares  <- floor(monthly_invest / price_i)
    cost        <- new_shares * price_i
    shares_dca  <- shares_dca + new_shares
    cash_dca    <- cash_dca   - cost
    total_invested <- total_invested + cost
  }

  total_value <- cash_dca + shares_dca * price_i
  portfolio_dca <- bind_rows(portfolio_dca,
                    data.frame(date = date_i, price = price_i,
                               shares = shares_dca, cash = cash_dca,
                               total_value, total_invested))
}

final_dca  <- tail(portfolio_dca$total_value, 1)
deployed   <- tail(portfolio_dca$total_invested, 1)
return_dca <- (final_dca - capital) / capital * 100
cat("DCA Strategy — Final Value: NT$", round(final_dca, 0),
    "| Total Deployed: NT$", round(deployed, 0),
    "| Total Return:", round(return_dca, 2), "%\n")
DCA Strategy — Final Value: NT$ 1791787 | Total Deployed: NT$ 989498 | Total Return: 79.18 %

5.2. Shares Accumulated Over Time

Because DCA buys at varying prices, shares accumulate unevenly. Let us visualize how the position builds month by month.

Code
# Monthly summary: date, shares held, price, value
portfolio_dca_monthly <- portfolio_dca |>
  mutate(ym = format(date, "%Y-%m")) |>
  group_by(ym) |>
  slice_max(date, n = 1) |>
  ungroup()

highchart() |>
  hc_title(text = "DCA — Shares Accumulated and Portfolio Value") |>
  hc_xAxis(type = "datetime") |>
  hc_yAxis_multiples(
    list(title = list(text = "Shares Held")),
    list(title = list(text = "Portfolio Value (NT$)"), opposite = TRUE)
  ) |>
  hc_add_series(portfolio_dca_monthly, type = "column",
                hcaes(x = date, y = shares),
                name = "Shares Held", yAxis = 0, color = "#93C5FD") |>
  hc_add_series(portfolio_dca_monthly, type = "line",
                hcaes(x = date, y = total_value),
                name = "Portfolio Value", yAxis = 1, color = "#234E70") |>
  hc_tooltip(shared = TRUE, valueDecimals = 0) |>
  hc_legend(enabled = T, align = "center", verticalAlign = "bottom") |>
  hc_add_theme(hc_theme_smpl())

5.3. Average Cost Per Share

A defining feature of DCA is the average cost per share — the total cash deployed divided by the total shares accumulated. When prices decline mid-period, DCA buys more shares at cheaper prices, lowering the average cost.

avg_cost <- deployed / tail(portfolio_dca$shares, 1)
last_price <- tail(etf_data$adjusted, 1)

cat("Total deployed: NT$", round(deployed, 0), "\n")
Total deployed: NT$ 989498 
cat("Shares accumulated:", tail(portfolio_dca$shares, 1), "\n")
Shares accumulated: 27537 
cat("Average cost per share: NT$", round(avg_cost, 2), "\n")
Average cost per share: NT$ 35.93 
cat("Final price: NT$", round(last_price, 2), "\n")
Final price: NT$ 64.69 
cat("Unrealized gain per share: NT$", round(last_price - avg_cost, 2), "\n")
Unrealized gain per share: NT$ 28.75 

6. Buy-and-Hold Benchmark

To make the comparison fair, we need a common benchmark. We invest NT$1,000,000 in 0050.TW on the first available date and hold to the end.

initial_bh_price <- etf_data$adjusted[1]
units_bh         <- floor(capital / initial_bh_price)
cash_bh          <- capital - units_bh * initial_bh_price

bh_portfolio <- etf_data |>
  mutate(bh_value = cash_bh + units_bh * adjusted) |>
  select(date, bh_value)

final_bh  <- tail(bh_portfolio$bh_value, 1)
return_bh <- (final_bh - capital) / capital * 100
cat("Buy-and-Hold — Final Value: NT$", round(final_bh, 0),
    "| Total Return:", round(return_bh, 2), "%\n")
Buy-and-Hold — Final Value: NT$ 2581160 | Total Return: 158.12 %

7. Grand Strategy Comparison

We now bring all five series together — the three technical strategies, DCA, and buy-and-hold — to compare them on equal footing. Because the strategies start at slightly different points (MA requires 50 days of warm-up; RSI requires 14 days; BB requires 20 days), we align all series from the first date available in the MA strategy to ensure a common start.

start_common <- min(portfolio_ma$date)

# Align all portfolios to the common start date
ma_aligned  <- portfolio_ma |>
  filter(date >= start_common) |>
  select(date, MA_Strategy = total_value)

rsi_aligned <- portfolio_rsi |>
  filter(date >= start_common) |>
  select(date, RSI_Strategy = total_value)

bb_aligned  <- portfolio_bb |>
  filter(date >= start_common) |>
  select(date, BB_Strategy = total_value)

dca_aligned <- portfolio_dca |>
  filter(date >= start_common) |>
  select(date, DCA_Strategy = total_value)

bh_aligned  <- bh_portfolio |>
  filter(date >= start_common) |>
  select(date, BH_0050 = bh_value)

# Merge all
comparison <- ma_aligned |>
  inner_join(rsi_aligned, by = "date") |>
  inner_join(bb_aligned,  by = "date") |>
  inner_join(dca_aligned, by = "date") |>
  inner_join(bh_aligned,  by = "date")

head(comparison)
        date MA_Strategy RSI_Strategy BB_Strategy DCA_Strategy   BH_0050
1 2023-01-03       1e+06        1e+06       1e+06    1000000.0 1000000.0
2 2023-01-04       1e+06        1e+06       1e+06     999932.3  997742.5
3 2023-01-05       1e+06        1e+06       1e+06    1000149.0 1004965.9
4 2023-01-06       1e+06        1e+06       1e+06    1000284.4 1009480.5
5 2023-01-09       1e+06        1e+06       1e+06    1001367.8 1045597.2
6 2023-01-10       1e+06        1e+06       1e+06    1001489.7 1049660.3

7.1. Portfolio Value Comparison

Code
highchart() |>
  hc_title(text = "Strategy Comparison — Portfolio Value (NT$)") |>
  hc_subtitle(text = "All strategies start with NT$1,000,000") |>
  hc_xAxis(type = "datetime") |>
  hc_yAxis(title = list(text = "Portfolio Value (NT$)")) |>
  hc_add_series(comparison, type = "line",
                hcaes(x = date, y = MA_Strategy),
                name = "MA Crossover") |>
  hc_add_series(comparison, type = "line",
                hcaes(x = date, y = RSI_Strategy),
                name = "RSI") |>
  hc_add_series(comparison, type = "line",
                hcaes(x = date, y = BB_Strategy),
                name = "Bollinger Bands") |>
  hc_add_series(comparison, type = "line",
                hcaes(x = date, y = DCA_Strategy),
                name = "DCA") |>
  hc_add_series(comparison, type = "line",
                hcaes(x = date, y = BH_0050),
                name = "Buy-and-Hold") |>
  hc_tooltip(valuePrefix = "NT$", valueDecimals = 0, shared = TRUE) |>
  hc_legend(enabled = T, align = "center", verticalAlign = "bottom") |>
  hc_add_theme(hc_theme_ft())

7.2. Performance Summary Table

Code
# Annualized volatility: std dev of daily returns * sqrt(252)
daily_ret <- comparison |>
  arrange(date) |>
  mutate(
    r_MA  = MA_Strategy  / lag(MA_Strategy)  - 1,
    r_RSI = RSI_Strategy / lag(RSI_Strategy) - 1,
    r_BB  = BB_Strategy  / lag(BB_Strategy)  - 1,
    r_DCA = DCA_Strategy / lag(DCA_Strategy) - 1,
    r_BH  = BH_0050      / lag(BH_0050)      - 1
  ) |>
  na.omit()

n_days   <- nrow(comparison)
ann_days <- 252

# Mean daily return (annualized) and volatility
summary_stats <- data.frame(
  Strategy       = c("MA Crossover", "RSI", "Bollinger Bands",
                     "DCA", "Buy-and-Hold"),
  Final_Value    = c(final_ma, final_rsi, final_bb, final_dca, final_bh),
  Total_Return   = round(c(return_ma, return_rsi, return_bb, return_dca, return_bh), 2),
  Ann_Vol        = round(c(sd(daily_ret$r_MA),  sd(daily_ret$r_RSI),
                            sd(daily_ret$r_BB),  sd(daily_ret$r_DCA),
                            sd(daily_ret$r_BH)) * sqrt(ann_days) * 100, 2),
  Sharpe         = NA_real_
)

# Sharpe ratio (assume 0% risk-free rate for simplicity)
summary_stats$Sharpe <- round(
  c(mean(daily_ret$r_MA),  mean(daily_ret$r_RSI),
    mean(daily_ret$r_BB),  mean(daily_ret$r_DCA),
    mean(daily_ret$r_BH)) /
  c(sd(daily_ret$r_MA),   sd(daily_ret$r_RSI),
    sd(daily_ret$r_BB),   sd(daily_ret$r_DCA),
    sd(daily_ret$r_BH)) * sqrt(ann_days), 2)

summary_stats$Final_Value <- paste0("NT$", formatC(round(summary_stats$Final_Value),
                                                    format = "d", big.mark = ","))

knitr::kable(summary_stats,
             col.names = c("Strategy", "Final Value", "Total Return (%)",
                           "Ann. Volatility (%)", "Sharpe Ratio"),
             align = "lrrrr",
             caption = "Performance Summary — All Strategies")
Performance Summary — All Strategies
Strategy Final Value Total Return (%) Ann. Volatility (%) Sharpe Ratio
MA Crossover NT$2,101,443 110.14 16.91 1.62
RSI NT$1,304,196 30.42 13.18 0.77
Bollinger Bands NT$1,568,357 56.84 14.50 1.15
DCA NT$1,791,787 79.18 15.91 1.36
Buy-and-Hold NT$2,581,160 158.12 21.99 1.61

8. Discussion: What Does This Tell Us?

8.1. Timing strategies vs. passive accumulation

The three technical strategies (MA, RSI, Bollinger Bands) all require the investor to be out of the market for some stretches. That idle cash earns nothing in our simulation. DCA, by contrast, is always partially in the market — it accumulates shares steadily, month by month.

8.2. The DCA perspective

DCA is not designed to maximize returns during a bull market — it will lag a lump-sum buy-and-hold when prices trend up monotonically. Its strength lies in reducing the regret risk of investing a large lump sum at a market peak. Consider two scenarios:

  • Lump-sum investor who puts in NT$1M on January 2, 2023.
  • DCA investor who puts in NT$20,000 per month from the same date.

If prices fall mid-year, the DCA investor buys additional shares at the lower price, effectively lowering their average cost. The lump-sum investor is fully exposed from day one and cannot benefit from the dip.

8.3. Risk-adjusted performance

The Sharpe ratio is the classic tool for comparing strategies that carry different levels of volatility. A strategy with a lower raw return but much lower volatility can have a higher Sharpe ratio than a more volatile winner. Look at the summary table above for the full picture.

What we ignored

All simulations above share several unrealistic assumptions that a real investor must account for:

  • Transaction costs: brokerage commissions, securities transaction taxes (0.3% in Taiwan on sales), and market impact.
  • Slippage: we assume trades execute at the daily close price, which is never guaranteed.
  • Bid-ask spread: 0050.TW is highly liquid, but the spread still exists.
  • Opportunity cost of idle cash: in the MA and RSI strategies, cash held between signals earns nothing.

Alternative Strategy Designs and Student Exercise

💡 Suggested Extensions

  1. RSI with MA filter: only act on RSI signals when the price is above MA200 (trending up). This avoids buying into a falling knife.
  2. MACD: the MACD() function in TTR computes the Moving Average Convergence/Divergence oscillator, which generates its own crossover signals.
  3. Stochastic Oscillator: stoch() in TTR. Similar logic to RSI but tracks where a closing price sits relative to its recent high-low range.
  4. DCA with a Boost: stick to NT$20,000/month normally, but invest NT$40,000 whenever RSI < 30 (doubling down when the market is oversold).
  5. Value Averaging: a variant of DCA where the target portfolio value grows by a fixed amount each month; you invest more when the market falls and less (or even sell) when it rises too fast.

🧠 Student Exercise

Design and backtest your own strategy for 0050.TW. Your task:

  1. Define a rule: use a technical indicator from TTR, a calendar effect, or a DCA variant.
  2. Implement it: adapt one of the simulation loops above.
  3. Benchmark it: compare to MA crossover, buy-and-hold, and DCA.
  4. Visualize: plot all series on one highcharter chart.
  5. Summarize: report final value, total return, annualized volatility, and Sharpe ratio.
  6. Reflect: in one paragraph, explain why your strategy did better or worse, and what you would change next.
Back to top