knitr::opts_chunk$set( collapse = TRUE, comment = "#>", fig.width = 7, fig.height = 5, message = FALSE, warning = FALSE )
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.
# Install from GitHub # (Skip during CRAN checks and vignette builds.) devtools::install_github("alb3rtazzo/PortfolioTesteR")
library(PortfolioTesteR)
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) summary(result1)
plot(result1, type = "performance")
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) summary(result2)
plot(result2, type = "performance")
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") print(result3_with) cat("\nWITHOUT Stop Loss:\n") print(result3_no)
# Plot both separately to avoid cramped figures plot(result3_with, type = "performance") plot(result3_no, type = "performance")
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) summary(result4)
plot(result4, type = "performance")
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) summary(result5)
plot(result5, type = "performance")
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) summary(res_stochrsi)
plot(res_stochrsi, type = "performance")
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)
The PortfolioTesteR Pattern
Function Families
calc_momentum(), calc_rsi(), calc_rolling_volatility(), calc_moving_average()filter_top_n(), filter_above(), filter_between(), combine_filters()weight_equally(), weight_by_signal(), weight_by_rank(), weight_by_volatility(), combine_weights()run_backtest()align_to_timeframe(), invert_signal(), limit_positions(), switch_weights()analyze_performance(), summary(), plot()?run_backtest ?calc_momentum ?filter_top_n ?analyze_performance
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.2. https://github.com/AlbertoPallotta/PortfolioTesteR
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.