The hardware and bandwidth for this mirror is donated by dogado GmbH, the Webhosting and Full Service-Cloud Provider. Check out our Wordpress Tutorial.
If you wish to report a bug, or if you are interested in having us mirror your free-software or open-source project, please feel free to contact us at mirror[@]dogado.de.

Getting Started with PortfolioTesteR

PortfolioTesteR: Test Investment Strategies with English-Like Code

PortfolioTesteR makes quantitative investing accessible through intuitive, English-like syntax. This vignette walks through five strategies from beginner to advanced, all using the included sample data so it builds quickly and reliably.

Note: Some functions also support external data (e.g., yahoo_adapter()), but for CRAN-friendly vignettes we use the bundled datasets.

Installation

# Install from GitHub
# (Skip during CRAN checks and vignette builds.)
devtools::install_github("alb3rtazzo/PortfolioTesteR")

Load the Library

library(PortfolioTesteR)

Strategy 1: Simple Momentum (Beginner)

Buy the stocks with the highest 12-week returns, weight them equally, and backtest.

# Load included weekly prices
data(sample_prices_weekly)

# 1) Momentum signal
momentum <- calc_momentum(sample_prices_weekly, lookback = 12)

# 2) Select top 10 by momentum
selected <- filter_top_n(momentum, n = 10)

# 3) Equal weights
weights <- weight_equally(selected)

# 4) Backtest
result1 <- run_backtest(
  prices = sample_prices_weekly,
  weights = weights,
  initial_capital = 100000,
  name = "Simple Momentum"
)

# 5) Results
print(result1)
#> Backtest Result:  Simple Momentum 
#> =====================================
#> Period: 2017-01-06 to 2019-12-31 (158 observations)
#> Initial Capital: $1e+05
#> Final Value: $166,806
#> Total Return: 66.81%
#> Transactions: 1490
#> 
#> Annualized Return: 18.34%
#> Annualized Volatility: 13.03%
#> Sharpe Ratio: 1.41
#> Max Drawdown: -16.61%
summary(result1)
#> 
#> Detailed Summary:  Simple Momentum 
#> =====================================
#> 
#> Position Statistics:
#>   Average positions held: 10.0
#>   Max positions held: 10
#>   Periods fully invested: 158 (100.0%)
#> 
#> Transaction Summary:
#>   Total trades: 1490
#>   Avg trades per period: 9.4
#>   Annual turnover: 728.8%
#> 
#> Return Distribution:
#>   Mean return: 0.341%
#>   Median return: 0.520%
#>   Best period: 5.19%
#>   Worst period: -6.86%
#>   Positive periods: 97 (61.4%)
plot(result1, type = "performance")

Strategy 2: Multi-Signal Combination (Intermediate)

Combine momentum (good = high) and stability (good = low volatility). Select stocks that rank well on both, then combine weights.

# Need daily data for volatility
data(sample_prices_daily)

# A) Momentum (12-week)
momentum <- calc_momentum(sample_prices_weekly, lookback = 12)

# B) Daily volatility → align weekly → invert (low vol = high score)
daily_vol <- calc_rolling_volatility(sample_prices_daily, lookback = 20)
weekly_vol <- align_to_timeframe(
  high_freq_data = daily_vol,
  low_freq_dates = sample_prices_weekly$Date,
  method = "forward_fill"
)
stability_signal <- invert_signal(weekly_vol)

# Select top 20 for each signal
m_sel <- filter_top_n(momentum, n = 20)
s_sel <- filter_top_n(stability_signal, n = 20)

# AND-combine the selections
both <- combine_filters(m_sel, s_sel, op = "and")

# Weight each way then blend 60/40
w_mom <- weight_by_signal(both, momentum)
w_stab <- weight_by_signal(both, stability_signal)
weights2 <- combine_weights(list(w_mom, w_stab), weights = c(0.6, 0.4))

# Backtest
result2 <- run_backtest(
  prices = sample_prices_weekly,
  weights = weights2,
  initial_capital = 100000,
  name = "Momentum + Low Vol"
)

print(result2)
#> Backtest Result:  Momentum + Low Vol 
#> =====================================
#> Warmup Period: 4 observations (no trading)
#> Active Period: 2017-02-03 to 2019-12-31 (154 observations)
#> Initial Capital: $1e+05
#> Final Value: $165,430
#> Total Return (active period): 65.43%
#> Total Return (full period): 65.43%
#> Transactions: 2579
#> 
#> Annualized Return: 18.53%
#> Annualized Volatility: 12.65%
#> Sharpe Ratio: 1.46
#> Max Drawdown: -13.99%
summary(result2)
#> 
#> Detailed Summary:  Momentum + Low Vol 
#> =====================================
#> 
#> Position Statistics:
#>   Average positions held: 19.5
#>   Max positions held: 20
#>   Periods fully invested: 62 (39.2%)
#> 
#> Transaction Summary:
#>   Total trades: 2579
#>   Avg trades per period: 16.3
#>   Annual turnover: 574.1%
#> 
#> Return Distribution:
#>   Mean return: 0.334%
#>   Median return: 0.557%
#>   Best period: 5.45%
#>   Worst period: -5.93%
#>   Positive periods: 98 (62.0%)
plot(result2, type = "performance")

Strategy 3: Risk-Managed Momentum with Stop Loss (Intermediate)

Add a 15% stop loss monitored on daily prices. (We compare the same strategy with and without stops.)

# Signals and selection
momentum <- calc_momentum(sample_prices_weekly, lookback = 12)
sel <- filter_top_n(momentum, n = 10)
weights_mom <- weight_by_signal(sel, momentum)

# With 15% stop-loss (daily monitoring)
result3_with <- run_backtest(
  prices = sample_prices_weekly,
  weights = weights_mom,
  initial_capital = 100000,
  name = "Momentum with 15% Stop Loss",
  stop_loss = 0.15,
  stop_monitoring_prices = sample_prices_daily
)

# Without stop-loss
result3_no <- run_backtest(
  prices = sample_prices_weekly,
  weights = weights_mom,
  initial_capital = 100000,
  name = "Momentum without Stop Loss"
)

cat("WITH Stop Loss:\n")
#> WITH Stop Loss:
print(result3_with)
#> Backtest Result:  Momentum with 15% Stop Loss 
#> =====================================
#> Warmup Period: 12 observations (no trading)
#> Active Period: 2017-03-31 to 2019-12-31 (146 observations)
#> Initial Capital: $1e+05
#> Final Value: $162,260
#> Total Return (active period): 62.26%
#> Total Return (full period): 62.26%
#> Transactions: 1513
#> 
#> Annualized Return: 18.81%
#> Annualized Volatility: 13.60%
#> Sharpe Ratio: 1.38
#> Max Drawdown: -12.28%
cat("\nWITHOUT Stop Loss:\n")
#> 
#> WITHOUT Stop Loss:
print(result3_no)
#> Backtest Result:  Momentum without Stop Loss 
#> =====================================
#> Warmup Period: 12 observations (no trading)
#> Active Period: 2017-03-31 to 2019-12-31 (146 observations)
#> Initial Capital: $1e+05
#> Final Value: $162,260
#> Total Return (active period): 62.26%
#> Total Return (full period): 62.26%
#> Transactions: 1513
#> 
#> Annualized Return: 18.81%
#> Annualized Volatility: 13.60%
#> Sharpe Ratio: 1.38
#> Max Drawdown: -12.28%
# Plot both separately to avoid cramped figures
plot(result3_with, type = "performance")

plot(result3_no, type = "performance")

Strategy 4: Regime-Adaptive Volatility (Advanced)

Detect market volatility regimes using SPY’s rolling volatility. Use defensive weights in high-vol regimes and aggressive weights in low-vol regimes.

# Extract SPY for regime detection
spy_prices <- sample_prices_weekly[, .(Date, SPY)]

# Trading universe (exclude SPY)
trading_symbols <- setdiff(names(sample_prices_weekly), c("Date", "SPY"))
trading_prices <- sample_prices_weekly[, c("Date", trading_symbols), with = FALSE]
trading_daily  <- sample_prices_daily[,  c("Date", trading_symbols), with = FALSE]

# SPY weekly returns & 20-week rolling volatility (annualized)
spy_returns <- c(NA, diff(spy_prices$SPY) / head(spy_prices$SPY, -1))
spy_vol <- zoo::rollapply(spy_returns, width = 20, FUN = sd, fill = NA, align = "right") * sqrt(52)

# High-vol regime = above median
vol_threshold <- median(spy_vol, na.rm = TRUE)
high_vol <- spy_vol > vol_threshold

# Selection by momentum
mom <- calc_momentum(trading_prices, lookback = 12)
sel <- filter_top_n(mom, n = 15)

# Defensive (prefer low vol) vs Aggressive (prefer high vol) weights
w_def <- weight_by_volatility(
  selected_df = sel,
  vol_timeframe_data = trading_daily,
  strategy_timeframe_data = trading_prices,
  lookback_periods = 20,
  low_vol_preference = TRUE,
  vol_method = "std"
)

w_agg <- weight_by_volatility(
  selected_df = sel,
  vol_timeframe_data = trading_daily,
  strategy_timeframe_data = trading_prices,
  lookback_periods = 20,
  low_vol_preference = FALSE,
  vol_method = "std"
)

# Switch weights by regime (defensive when high-vol is TRUE)
weights4 <- switch_weights(
  weights_a = w_agg,  # used when condition is FALSE (low vol)
  weights_b = w_def,  # used when condition is TRUE  (high vol)
  use_b_condition = high_vol
)

result4 <- run_backtest(
  prices = trading_prices,
  weights = weights4,
  initial_capital = 100000,
  name = "Regime-Adaptive Strategy"
)

print(result4)
#> Backtest Result:  Regime-Adaptive Strategy 
#> =====================================
#> Warmup Period: 4 observations (no trading)
#> Active Period: 2017-02-03 to 2019-12-31 (154 observations)
#> Initial Capital: $1e+05
#> Final Value: $144,325
#> Total Return (active period): 44.32%
#> Total Return (full period): 44.32%
#> Transactions: 2273
#> 
#> Annualized Return: 13.19%
#> Annualized Volatility: 12.91%
#> Sharpe Ratio: 1.02
#> Max Drawdown: -18.46%
summary(result4)
#> 
#> Detailed Summary:  Regime-Adaptive Strategy 
#> =====================================
#> 
#> Position Statistics:
#>   Average positions held: 14.6
#>   Max positions held: 15
#>   Periods fully invested: 130 (82.3%)
#> 
#> Transaction Summary:
#>   Total trades: 2273
#>   Avg trades per period: 14.4
#>   Annual turnover: 940.3%
#> 
#> Return Distribution:
#>   Mean return: 0.248%
#>   Median return: 0.404%
#>   Best period: 4.41%
#>   Worst period: -5.85%
#>   Positive periods: 92 (58.2%)
plot(result4, type = "performance")

Strategy 5: Multi-Factor with Position Limits (Advanced)

Combine momentum and stability signals, then enforce a max positions limit to control concentration. Weight 70% by momentum strength and 30% by stability.

# Signals
momentum <- calc_momentum(sample_prices_weekly, lookback = 12)
daily_vol <- calc_rolling_volatility(sample_prices_daily, lookback = 20)
weekly_vol <- align_to_timeframe(daily_vol, sample_prices_weekly$Date, method = "forward_fill")
stability <- invert_signal(weekly_vol)

# Selection & position cap
top30 <- filter_top_n(momentum, n = 30)
sel15 <- limit_positions(top30, momentum, max_positions = 15)

# Weights and blend (70/30)
w_m <- weight_by_signal(sel15, momentum)
w_s <- weight_by_signal(sel15, stability)
weights5 <- combine_weights(list(w_m, w_s), weights = c(0.7, 0.3))

# Backtest
result5 <- run_backtest(
  prices = sample_prices_weekly,
  weights = weights5,
  initial_capital = 100000,
  name = "Multi-Factor with Position Limits"
)

print(result5)
#> Backtest Result:  Multi-Factor with Position Limits 
#> =====================================
#> Warmup Period: 4 observations (no trading)
#> Active Period: 2017-02-03 to 2019-12-31 (154 observations)
#> Initial Capital: $1e+05
#> Final Value: $163,629
#> Total Return (active period): 63.63%
#> Total Return (full period): 63.63%
#> Transactions: 2281
#> 
#> Annualized Return: 18.09%
#> Annualized Volatility: 12.92%
#> Sharpe Ratio: 1.40
#> Max Drawdown: -13.56%
summary(result5)
#> 
#> Detailed Summary:  Multi-Factor with Position Limits 
#> =====================================
#> 
#> Position Statistics:
#>   Average positions held: 14.6
#>   Max positions held: 15
#>   Periods fully invested: 115 (72.8%)
#> 
#> Transaction Summary:
#>   Total trades: 2281
#>   Avg trades per period: 14.4
#>   Annual turnover: 781.0%
#> 
#> Return Distribution:
#>   Mean return: 0.328%
#>   Median return: 0.480%
#>   Best period: 5.34%
#>   Worst period: -6.19%
#>   Positive periods: 99 (62.7%)
plot(result5, type = "performance")

Strategy 6: StochRSI Acceleration + Inverse-Vol Risk Parity

Advanced Strategy: StochRSI Acceleration + Inverse-Vol Risk Parity - Gate to high StochRSI (>= 0.80), then select top-12 by acceleration - Allocate by inverse-volatility risk parity using DAILY prices - Backtest on the weekly grid (bundled datasets only; CRAN-friendly)

# Data
data(sample_prices_weekly)
data(sample_prices_daily)

# Exclude broad ETFs from stock-selection universe
symbols_all   <- setdiff(names(sample_prices_weekly), "Date")
stock_symbols <- setdiff(symbols_all, c("SPY", "TLT"))

weekly_stocks <- sample_prices_weekly[, c("Date", stock_symbols), with = FALSE]
daily_stocks  <- sample_prices_daily[,  c("Date", stock_symbols), with = FALSE]

# StochRSI "acceleration" signal (weekly)
stochrsi    <- calc_stochrsi(weekly_stocks, length = 14)   # in [0,1]
stochrsi_ma <- calc_moving_average(stochrsi, window = 5)
accel       <- calc_distance(stochrsi, stochrsi_ma)        # positive = rising

# Gate to high StochRSI zone, then take top-12 by acceleration
high_zone <- filter_above(stochrsi, value = 0.80)
sel <- filter_top_n_where(
  signal_df     = accel,
  n             = 12,
  condition_df  = high_zone,
  min_qualified = 8,
  ascending     = FALSE
)

# Allocation: inverse-volatility risk parity (DAILY prices)
w_ivol <- weight_by_risk_parity(
  selected_df      = sel,
  prices_df        = daily_stocks,
  method           = "inverse_vol",
  lookback_periods = 126,  # ~6 months
  min_periods      = 60
)

# Backtest on the weekly grid
res_stochrsi <- run_backtest(
  prices          = weekly_stocks,
  weights         = w_ivol,
  initial_capital = 100000,
  name            = "StochRSI Accel + InvVol RP"
)

print(res_stochrsi)
#> Backtest Result:  StochRSI Accel + InvVol RP 
#> =====================================
#> Warmup Period: 30 observations (no trading)
#> Active Period: 2017-08-04 to 2019-12-31 (128 observations)
#> Initial Capital: $1e+05
#> Final Value: $115,642
#> Total Return (active period): 15.64%
#> Total Return (full period): 15.64%
#> Transactions: 682
#> 
#> Annualized Return: 6.08%
#> Annualized Volatility: 6.29%
#> Sharpe Ratio: 0.97
#> Max Drawdown: -6.15%
summary(res_stochrsi)
#> 
#> Detailed Summary:  StochRSI Accel + InvVol RP 
#> =====================================
#> 
#> Position Statistics:
#>   Average positions held: 3.3
#>   Max positions held: 12
#>   Periods fully invested: 50 (31.6%)
#> 
#> Transaction Summary:
#>   Total trades: 682
#>   Avg trades per period: 4.3
#>   Annual turnover: 703.0%
#> 
#> Return Distribution:
#>   Mean return: 0.095%
#>   Median return: 0.000%
#>   Best period: 3.13%
#>   Worst period: -3.72%
#>   Positive periods: 32 (20.3%)
plot(res_stochrsi, type = "performance")

Optional: Live Data via Yahoo (CRAN-safe)

Below is a minimal example that fetches prices from Yahoo Finance and runs the same “calculate → filter → weight → backtest” pipeline. The code is disabled inside CRAN/devtools checks. To run it locally, set Sys.setenv(RUN_LIVE = "true") before knitting.

library(PortfolioTesteR)

# Fetch weekly data for a small set of tickers
tickers <- c("AAPL","MSFT","AMZN","GOOGL","META")
px_weekly <- yahoo_adapter(
  symbols   = tickers,
  frequency = "weekly"
)

# Simple momentum: top-3 by 12-week return, equal weight
mom  <- calc_momentum(px_weekly, lookback = 12)
sel  <- filter_top_n(mom, n = 3)
w_eq <- weight_equally(sel)

res_yh <- run_backtest(
  prices          = px_weekly,
  weights         = w_eq,
  initial_capital = 100000,
  name            = "Yahoo: Simple Momentum (Top 3)"
)

print(res_yh)
summary(res_yh)

Key Concepts Recap

The PortfolioTesteR Pattern

  1. Calculate signals (momentum, volatility, etc.)
  2. Filter the universe (top-N, thresholds, logical combinations)
  3. Weight the portfolio (equally, by signal, by volatility, etc.)
  4. Backtest the strategy
  5. Analyze with built-in metrics and visualizations

Function Families

Getting Help

?run_backtest
?calc_momentum
?filter_top_n
?analyze_performance

Citation

If you use PortfolioTesteR in your research, please cite:

Pallotta, A. (2025). PortfolioTesteR: Test Investment Strategies with English-Like Code. R package version 0.1.0. https://github.com/alb3rtazzo/PortfolioTesteR

These binaries (installable software) and packages are in development.
They may not be fully stable and should be used with caution. We make no claims about them.
Health stats visible at Monitor.