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:
Moving Average (MA) Crossover — a trend-following strategy based on short- vs. long-term moving averages.
RSI Strategy — a mean-reversion strategy using the Relative Strength Index.
Bollinger Bands Strategy — a volatility-based mean-reversion strategy.
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.
capital <-1000000position <-0cash <- capitalportfolio_ma <-data.frame()for (i inseq_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 } elseif (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 *100cat("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 annotationbuy_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.
position <-0cash <- capitalportfolio_rsi <-data.frame()for (i inseq_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 } elseif (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 *100cat("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 markersp1 <-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 bandsp2 <-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).
position <-0cash <- capitalportfolio_bb <-data.frame()for (i inseq_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 } elseif (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 *100cat("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 monthtotal_reserve <-1000000# cash reserve (never more than this deployed in total)# Identify the first trading day of each monthfirst_trading_days <- etf_data |>mutate(ym =format(date, "%Y-%m")) |>group_by(ym) |>slice_min(date, n =1) |>ungroup() |>select(date) |>pull()# Simulate DCAcash_dca <- total_reserveshares_dca <-0portfolio_dca <-data.frame()total_invested <-0# track cumulative cash deployedfor (i inseq_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 *100cat("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, valueportfolio_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.
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 datema_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 allcomparison <- 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)
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
RSI with MA filter: only act on RSI signals when the price is above MA200 (trending up). This avoids buying into a falling knife.
MACD: the MACD() function in TTR computes the Moving Average Convergence/Divergence oscillator, which generates its own crossover signals.
Stochastic Oscillator: stoch() in TTR. Similar logic to RSI but tracks where a closing price sits relative to its recent high-low range.
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).
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:
Define a rule: use a technical indicator from TTR, a calendar effect, or a DCA variant.
Implement it: adapt one of the simulation loops above.
Benchmark it: compare to MA crossover, buy-and-hold, and DCA.
Visualize: plot all series on one highcharter chart.
Summarize: report final value, total return, annualized volatility, and Sharpe ratio.
Reflect: in one paragraph, explain why your strategy did better or worse, and what you would change next.