# 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

- Indicators: `calc_momentum()`, `calc_rsi()`, `calc_rolling_volatility()`, `calc_moving_average()`
- Selection: `filter_top_n()`, `filter_above()`, `filter_between()`, `combine_filters()`
- Weighting: `weight_equally()`, `weight_by_signal()`, `weight_by_rank()`, `weight_by_volatility()`, `combine_weights()`
- Execution: `run_backtest()`
- Utilities: `align_to_timeframe()`, `invert_signal()`, `limit_positions()`, `switch_weights()`
- Analysis: `analyze_performance()`, `summary()`, `plot()`

## 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.2. https://github.com/AlbertoPallotta/PortfolioTesteR