| Title: | Test Investment Strategies with English-Like Code |
|---|---|
| Description: | Design, backtest, and analyze portfolio strategies using simple, English-like function chains. Includes technical indicators, flexible stock selection, portfolio construction methods (equal weighting, signal weighting, inverse volatility, hierarchical risk parity), and a compact backtesting engine for portfolio returns, drawdowns, and summary metrics. |
| Authors: | Alberto Pallotta [aut, cre] |
| Maintainer: | Alberto Pallotta <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 0.1.4 |
| Built: | 2026-05-31 08:28:40 UTC |
| Source: | https://github.com/albertopallotta/portfoliotester |
Aligns higher-frequency data to match strategy timeframe.
align_to_timeframe( high_freq_data, low_freq_dates, method = c("forward_fill", "nearest", "interpolate") )align_to_timeframe( high_freq_data, low_freq_dates, method = c("forward_fill", "nearest", "interpolate") )
high_freq_data |
Data frame to align |
low_freq_dates |
Date vector from strategy |
method |
Alignment method: "forward_fill", "nearest", or "interpolate" |
Aligned data frame
data("sample_prices_weekly") data("sample_prices_daily") momentum <- calc_momentum(sample_prices_weekly, lookback = 12) selected <- filter_top_n(momentum, 10) # Create a stability signal from daily data daily_vol <- calc_rolling_volatility(sample_prices_daily, lookback = 20) stability_signal <- align_to_timeframe(daily_vol, sample_prices_weekly$Date) weights <- weight_by_signal(selected, stability_signal)data("sample_prices_weekly") data("sample_prices_daily") momentum <- calc_momentum(sample_prices_weekly, lookback = 12) selected <- filter_top_n(momentum, 10) # Create a stability signal from daily data daily_vol <- calc_rolling_volatility(sample_prices_daily, lookback = 20) stability_signal <- align_to_timeframe(daily_vol, sample_prices_weekly$Date) weights <- weight_by_signal(selected, stability_signal)
Aggregates portfolio results by calendar period and computes standard statistics
for each period. Provide at least one of returns or values.
analyze_by_period( dates, returns = NULL, values = NULL, period = c("monthly", "quarterly", "yearly"), na_rm = TRUE )analyze_by_period( dates, returns = NULL, values = NULL, period = c("monthly", "quarterly", "yearly"), na_rm = TRUE )
dates |
Date vector aligned to |
returns |
Numeric simple returns aligned to |
values |
Numeric equity values aligned to |
period |
"monthly", "quarterly", or "yearly". |
na_rm |
Logical; remove NAs inside per-period aggregations. |
data.frame with period keys and columns: ret, start_value, end_value, n_obs.
data(sample_prices_weekly) mom12 <- PortfolioTesteR::calc_momentum(sample_prices_weekly, lookback = 12) sel5 <- PortfolioTesteR::filter_top_n(mom12, n = 5) w_eq <- PortfolioTesteR::weight_equally(sel5) pr <- PortfolioTesteR::portfolio_returns(w_eq, sample_prices_weekly) val <- 1e5 * cumprod(1 + pr$portfolio_return) out <- analyze_by_period( dates = pr$Date, returns = pr$portfolio_return, values = val, period = "monthly" ) head(out)data(sample_prices_weekly) mom12 <- PortfolioTesteR::calc_momentum(sample_prices_weekly, lookback = 12) sel5 <- PortfolioTesteR::filter_top_n(mom12, n = 5) w_eq <- PortfolioTesteR::weight_equally(sel5) pr <- PortfolioTesteR::portfolio_returns(w_eq, sample_prices_weekly) val <- 1e5 * cumprod(1 + pr$portfolio_return) out <- analyze_by_period( dates = pr$Date, returns = pr$portfolio_return, values = val, period = "monthly" ) head(out)
Detailed analysis of drawdown periods including depth, duration, and recovery.
analyze_drawdowns(drawdowns, returns)analyze_drawdowns(drawdowns, returns)
drawdowns |
Drawdown series (negative values) |
returns |
Return series for additional metrics |
List with drawdown statistics
data("sample_prices_weekly") momentum <- calc_momentum(sample_prices_weekly, lookback = 12) selected <- filter_top_n(momentum, n = 10) weights <- weight_equally(selected) result <- run_backtest(sample_prices_weekly, weights) dd_analysis <- analyze_drawdowns(result$portfolio_value, result$dates)data("sample_prices_weekly") momentum <- calc_momentum(sample_prices_weekly, lookback = 12) selected <- filter_top_n(momentum, n = 10) weights <- weight_equally(selected) result <- run_backtest(sample_prices_weekly, weights) dd_analysis <- analyze_drawdowns(result$portfolio_value, result$dates)
Calculates comprehensive performance metrics using daily price data for enhanced accuracy. Provides risk-adjusted returns, drawdown analysis, and benchmark comparison even when strategy trades at lower frequency.
analyze_performance( backtest_result, daily_prices, benchmark_symbol = "SPY", rf_rate = 0, confidence_level = 0.95 )analyze_performance( backtest_result, daily_prices, benchmark_symbol = "SPY", rf_rate = 0, confidence_level = 0.95 )
backtest_result |
Result object from run_backtest() |
daily_prices |
Daily price data including all portfolio symbols |
benchmark_symbol |
Symbol for benchmark comparison (default: "SPY") |
rf_rate |
Annual risk-free rate for Sharpe/Sortino (default: 0) |
confidence_level |
Confidence level for VaR/CVaR (default: 0.95) |
performance_analysis object with metrics and daily tracking
data("sample_prices_weekly") data("sample_prices_daily") # Use overlapping symbols; cap to 3 syms_all <- intersect(names(sample_prices_weekly)[-1], names(sample_prices_daily)[-1]) stopifnot(length(syms_all) >= 1) syms <- syms_all[seq_len(min(3L, length(syms_all)))] # Subset weekly (strategy) and daily (monitoring) to the same symbols P <- sample_prices_weekly[, c("Date", syms), with = FALSE] D <- sample_prices_daily[, c("Date", syms), with = FALSE] # Simple end-to-end example mom <- calc_momentum(P, lookback = 12) sel <- filter_top_n(mom, n = 3) W <- weight_equally(sel) res <- run_backtest(P, W) # Pick a benchmark that is guaranteed to exist in D perf <- analyze_performance(res, D, benchmark_symbol = syms[1]) print(perf) summary(perf)data("sample_prices_weekly") data("sample_prices_daily") # Use overlapping symbols; cap to 3 syms_all <- intersect(names(sample_prices_weekly)[-1], names(sample_prices_daily)[-1]) stopifnot(length(syms_all) >= 1) syms <- syms_all[seq_len(min(3L, length(syms_all)))] # Subset weekly (strategy) and daily (monitoring) to the same symbols P <- sample_prices_weekly[, c("Date", syms), with = FALSE] D <- sample_prices_daily[, c("Date", syms), with = FALSE] # Simple end-to-end example mom <- calc_momentum(P, lookback = 12) sel <- filter_top_n(mom, n = 3) W <- weight_equally(sel) res <- run_backtest(P, W) # Pick a benchmark that is guaranteed to exist in D perf <- analyze_performance(res, D, benchmark_symbol = syms[1]) print(perf) summary(perf)
Computes standard benchmark-relative metrics (e.g., correlation, beta, alpha, tracking error, information ratio) by aligning portfolio returns with benchmark returns derived from prices.
analyze_vs_benchmark( portfolio_returns, benchmark_prices, dates, benchmark_symbol = "SPY" )analyze_vs_benchmark( portfolio_returns, benchmark_prices, dates, benchmark_symbol = "SPY" )
portfolio_returns |
A numeric vector of portfolio simple returns aligned to |
benchmark_prices |
A data frame (Date + symbols) of adjusted benchmark prices at the
same cadence as |
dates |
A vector of |
benchmark_symbol |
Character scalar giving the column name (symbol) in |
A list or data frame with benchmark-relative statistics according to the package's conventions, including correlation, beta, alpha, tracking error, and information ratio.
data(sample_prices_weekly) mom12 <- PortfolioTesteR::calc_momentum(sample_prices_weekly, lookback = 12) sel10 <- PortfolioTesteR::filter_top_n(mom12, n = 5) w_eq <- PortfolioTesteR::weight_equally(sel10) pr <- PortfolioTesteR::portfolio_returns(w_eq, sample_prices_weekly) # Use SPY as the benchmark bench <- sample_prices_weekly[, c("Date", "SPY")] res <- analyze_vs_benchmark( pr$portfolio_return, bench, dates = pr$Date, benchmark_symbol = "SPY" ) resdata(sample_prices_weekly) mom12 <- PortfolioTesteR::calc_momentum(sample_prices_weekly, lookback = 12) sel10 <- PortfolioTesteR::filter_top_n(mom12, n = 5) w_eq <- PortfolioTesteR::weight_equally(sel10) pr <- PortfolioTesteR::portfolio_returns(w_eq, sample_prices_weekly) # Use SPY as the benchmark bench <- sample_prices_weekly[, c("Date", "SPY")] res <- analyze_vs_benchmark( pr$portfolio_return, bench, dates = pr$Date, benchmark_symbol = "SPY" ) res
Applies regime-based filtering. When regime is FALSE (e.g., bear market), all selections become 0, moving portfolio to cash.
apply_regime(selection_df, regime_condition, partial_weight = 0)apply_regime(selection_df, regime_condition, partial_weight = 0)
selection_df |
Binary selection matrix |
regime_condition |
Logical vector (TRUE = trade, FALSE = cash) |
partial_weight |
Fraction to hold when regime is FALSE (default: 0) |
Modified selection matrix respecting regime
data("sample_prices_weekly") # Create selection momentum <- calc_momentum(sample_prices_weekly, 12) selected <- filter_top_n(momentum, 10) # Only trade when SPY above 20-week MA ma20 <- calc_moving_average(sample_prices_weekly, 20) spy_regime <- sample_prices_weekly$SPY > ma20$SPY spy_regime[is.na(spy_regime)] <- FALSE regime_filtered <- apply_regime(selected, spy_regime)data("sample_prices_weekly") # Create selection momentum <- calc_momentum(sample_prices_weekly, 12) selected <- filter_top_n(momentum, 10) # Only trade when SPY above 20-week MA ma20 <- calc_moving_average(sample_prices_weekly, 20) spy_regime <- sample_prices_weekly$SPY > ma20$SPY spy_regime[is.na(spy_regime)] <- FALSE regime_filtered <- apply_regime(selected, spy_regime)
Converts condition matrices or data frames to standard selection format with Date column and binary values. Handles NA by converting to 0.
as_selection(condition_matrix, date_column = NULL)as_selection(condition_matrix, date_column = NULL)
condition_matrix |
Matrix or data frame with conditions |
date_column |
Optional Date vector if not in input |
Data.table in selection format (Date + binary columns)
data("sample_prices_weekly") ma20 <- calc_moving_average(sample_prices_weekly, 20) above_ma <- filter_above(calc_distance(sample_prices_weekly, ma20), 0) selection <- as_selection(above_ma, sample_prices_weekly$Date)data("sample_prices_weekly") ma20 <- calc_moving_average(sample_prices_weekly, 20) above_ma <- filter_above(calc_distance(sample_prices_weekly, ma20), 0) selection <- as_selection(above_ma, sample_prices_weekly$Date)
Computes performance metrics including Sharpe ratio, maximum drawdown, win rate, and other statistics from backtest results.
backtest_metrics(result)backtest_metrics(result)
result |
Backtest result object from run_backtest() |
List containing performance metrics
# Create a backtest result to use data(sample_prices_weekly) momentum <- calc_momentum(sample_prices_weekly, lookback = 12) selected <- filter_top_n(momentum, n = 10) weights <- weight_equally(selected) result <- run_backtest(sample_prices_weekly, weights) # Calculate metrics metrics <- backtest_metrics(result) print(metrics$sharpe_ratio)# Create a backtest result to use data(sample_prices_weekly) momentum <- calc_momentum(sample_prices_weekly, lookback = 12) selected <- filter_top_n(momentum, n = 10) weights <- weight_equally(selected) result <- run_backtest(sample_prices_weekly, weights) # Calculate metrics metrics <- backtest_metrics(result) print(metrics$sharpe_ratio)
Bucketed label analysis by score rank
bucket_returns( scores, labels, n_buckets = 10L, label_type = c("log", "simple") )bucket_returns( scores, labels, n_buckets = 10L, label_type = c("log", "simple") )
scores |
Wide score panel (Date + symbols). |
labels |
Wide label panel aligned to scores (Date + symbols). |
n_buckets |
Number of equal-count buckets. |
label_type |
Use 'log' or 'simple' label arithmetic. |
data.table of per-date bucket returns; cumulative series attached
as attr(result, "cum").
Calculates CCI using closing prices. CCI measures deviation from average price. Values above 100 indicate overbought, below -100 indicate oversold.
calc_cci(data, period = 20)calc_cci(data, period = 20)
data |
Data frame with Date column and price columns |
period |
CCI period (default: 20) |
Data.table with CCI values
data("sample_prices_weekly") cci <- calc_cci(sample_prices_weekly, period = 20)data("sample_prices_weekly") cci <- calc_cci(sample_prices_weekly, period = 20)
data("sample_prices_weekly") Calculates percentage distance between prices and reference values (typically moving averages).
calc_distance(price_df, reference_df)calc_distance(price_df, reference_df)
price_df |
Data frame with price data |
reference_df |
Data frame with reference values (same structure) |
Data.table with percentage distances
data("sample_prices_weekly") ma20 <- calc_moving_average(sample_prices_weekly, 20) data("sample_prices_weekly") distance <- calc_distance(sample_prices_weekly, ma20)data("sample_prices_weekly") ma20 <- calc_moving_average(sample_prices_weekly, 20) data("sample_prices_weekly") distance <- calc_distance(sample_prices_weekly, ma20)
Measures the percentage of stocks meeting a condition (market participation). Useful for assessing market health and identifying broad vs narrow moves.
calc_market_breadth(condition_df, min_stocks = 10)calc_market_breadth(condition_df, min_stocks = 10)
condition_df |
Data frame with Date column and TRUE/FALSE values |
min_stocks |
Minimum stocks required for valid calculation (default: 10) |
A data.table with Date and Breadth_[Sector] columns (0-100 scale)
# Percent of stocks above 200-day MA data("sample_prices_weekly") ma200 <- calc_moving_average(sample_prices_weekly, 200) above_ma <- filter_above(calc_distance(sample_prices_weekly, ma200), 0) breadth <- calc_market_breadth(above_ma)# Percent of stocks above 200-day MA data("sample_prices_weekly") ma200 <- calc_moving_average(sample_prices_weekly, 200) above_ma <- filter_above(calc_distance(sample_prices_weekly, ma200), 0) breadth <- calc_market_breadth(above_ma)
Calculates momentum as the percentage change in price over a specified lookback period. Optimized using column-wise operations (25x faster).
calc_momentum(data, lookback = 12)calc_momentum(data, lookback = 12)
data |
A data.frame or data.table with Date column and price columns |
lookback |
Number of periods for momentum calculation (default: 12) |
Data.table with momentum values (0.1 = 10% increase)
data("sample_prices_weekly") momentum <- calc_momentum(sample_prices_weekly, lookback = 12)data("sample_prices_weekly") momentum <- calc_momentum(sample_prices_weekly, lookback = 12)
Calculates simple moving average for each column in the data.
calc_moving_average(data, window = 20)calc_moving_average(data, window = 20)
data |
Data frame with Date column and price columns |
window |
Number of periods for moving average (default: 20) |
Data.table with moving average values
data("sample_prices_weekly") ma20 <- calc_moving_average(sample_prices_weekly, window = 20)data("sample_prices_weekly") ma20 <- calc_moving_average(sample_prices_weekly, window = 20)
Ranks each stock's indicator value against all other stocks on the same date. Enables relative strength strategies that adapt to market conditions. Optimized using matrix operations for 15x speedup.
calc_relative_strength_rank( indicator_df, method = c("percentile", "rank", "z-score") )calc_relative_strength_rank( indicator_df, method = c("percentile", "rank", "z-score") )
indicator_df |
Data frame with Date column and indicator values |
method |
Ranking method: "percentile" (0-100), "rank" (1-N), or "z-score" |
Data frame with same structure containing ranks/scores
# Rank RSI across all stocks data("sample_prices_weekly") rsi <- calc_rsi(sample_prices_weekly, 14) rsi_ranks <- calc_relative_strength_rank(rsi, method = "percentile") # Find relatively overbought (top 10%) relative_overbought <- filter_above(rsi_ranks, 90)# Rank RSI across all stocks data("sample_prices_weekly") rsi <- calc_rsi(sample_prices_weekly, 14) rsi_ranks <- calc_relative_strength_rank(rsi, method = "percentile") # Find relatively overbought (top 10%) relative_overbought <- filter_above(rsi_ranks, 90)
Computes rolling correlations between each symbol and a benchmark series
(e.g., SPY) using simple returns over a fixed lookback window.
calc_rolling_correlation( data, benchmark_symbol = "SPY", lookback = 60, min_periods = NULL, method = c("pearson", "spearman") )calc_rolling_correlation( data, benchmark_symbol = "SPY", lookback = 60, min_periods = NULL, method = c("pearson", "spearman") )
data |
A |
benchmark_symbol |
Character, the benchmark column name (default |
lookback |
Integer window size (>= 2) for rolling correlations. |
min_periods |
Minimum number of valid observations within the window
to compute a correlation. Default is |
method |
Correlation method, |
Returns are computed as simple returns .
Windows with fewer than min_periods valid pairs are marked NA.
A data.table with Date and one column per non-benchmark symbol,
containing rolling correlations. Insufficient data yields NAs.
calc_momentum(), calc_rolling_volatility()
data(sample_prices_weekly) corr <- calc_rolling_correlation( data = sample_prices_weekly, benchmark_symbol = "SPY", lookback = 20 ) head(corr)data(sample_prices_weekly) corr <- calc_rolling_correlation( data = sample_prices_weekly, benchmark_symbol = "SPY", lookback = 20 ) head(corr)
Calculates rolling volatility using various methods including standard deviation, range-based, MAD, or absolute returns. Supports different lookback periods.
calc_rolling_volatility(data, lookback = 20, method = "std")calc_rolling_volatility(data, lookback = 20, method = "std")
data |
Data frame with Date column and price columns |
lookback |
Number of periods for rolling calculation (default: 20) |
method |
Volatility calculation method: "std", "range", "mad", or "abs_return" |
Data frame with Date column and volatility values for each symbol
data("sample_prices_weekly") # Standard deviation volatility vol <- calc_rolling_volatility(sample_prices_weekly, lookback = 20) # Range-based volatility vol_range <- calc_rolling_volatility(sample_prices_weekly, lookback = 20, method = "range")data("sample_prices_weekly") # Standard deviation volatility vol <- calc_rolling_volatility(sample_prices_weekly, lookback = 20) # Range-based volatility vol_range <- calc_rolling_volatility(sample_prices_weekly, lookback = 20, method = "range")
Calculates RSI for each column. RSI ranges from 0-100. Above 70 indicates overbought, below 30 indicates oversold.
calc_rsi(data, period = 14)calc_rsi(data, period = 14)
data |
Data frame with Date column and price columns |
period |
RSI period (default: 14) |
Data.table with RSI values (0-100 range)
data("sample_prices_weekly") rsi <- calc_rsi(sample_prices_weekly, period = 14) overbought <- filter_above(rsi, 70)data("sample_prices_weekly") rsi <- calc_rsi(sample_prices_weekly, period = 14) overbought <- filter_above(rsi, 70)
Measures participation within each sector separately, revealing which sectors have broad strength vs concentrated leadership. Optimized using pre-splitting for speed.
calc_sector_breadth( condition_df, sector_mapping, min_stocks_per_sector = 3, na_sector_action = c("exclude", "separate", "market") )calc_sector_breadth( condition_df, sector_mapping, min_stocks_per_sector = 3, na_sector_action = c("exclude", "separate", "market") )
condition_df |
Data frame with Date column and TRUE/FALSE values |
sector_mapping |
Data frame with |
min_stocks_per_sector |
Minimum stocks for valid sector breadth (default: 3) |
na_sector_action |
How to handle unmapped stocks: "exclude", "separate", or "market" |
A data.table with Date and Breadth_[Sector] columns (0-100 scale)
data("sample_prices_weekly") data("sample_sp500_sectors") ma200 <- calc_moving_average(sample_prices_weekly, 200) above_ma <- filter_above(calc_distance(sample_prices_weekly, ma200), 0) sector_breadth <- calc_sector_breadth(above_ma, sample_sp500_sectors)data("sample_prices_weekly") data("sample_sp500_sectors") ma200 <- calc_moving_average(sample_prices_weekly, 200) above_ma <- filter_above(calc_distance(sample_prices_weekly, ma200), 0) sector_breadth <- calc_sector_breadth(above_ma, sample_sp500_sectors)
Measures how each stock's indicator compares to its sector benchmark. Enables sector-neutral strategies and identifies sector outperformers.
calc_sector_relative_indicators( indicator_df, sector_mapping, method = c("difference", "ratio", "z-score"), benchmark = c("mean", "median"), ratio_threshold = 0.01, min_sector_size = 2 )calc_sector_relative_indicators( indicator_df, sector_mapping, method = c("difference", "ratio", "z-score"), benchmark = c("mean", "median"), ratio_threshold = 0.01, min_sector_size = 2 )
indicator_df |
Data frame with Date column and indicator values |
sector_mapping |
Data frame with |
method |
"difference" (absolute), "ratio" (relative), or "z-score" |
benchmark |
"mean" or "median" sector average |
ratio_threshold |
Minimum denominator for ratio method (default: 0.01) |
min_sector_size |
Minimum stocks per sector (default: 2) |
Data frame with sector-relative values
# Find stocks outperforming their sector data("sample_prices_weekly") data("sample_sp500_sectors") momentum <- calc_momentum(sample_prices_weekly, 12) relative_momentum <- calc_sector_relative_indicators( momentum, sample_sp500_sectors, method = "difference" )# Find stocks outperforming their sector data("sample_prices_weekly") data("sample_sp500_sectors") momentum <- calc_momentum(sample_prices_weekly, 12) relative_momentum <- calc_sector_relative_indicators( momentum, sample_sp500_sectors, method = "difference" )
Calculates the Stochastic D indicator for momentum analysis. The %D line is the smoothed version of %K, commonly used for momentum signals in range 0-100.
calc_stochastic_d(data, k = 14, d = 3)calc_stochastic_d(data, k = 14, d = 3)
data |
Price data with Date column and symbol columns |
k |
Lookback period for stochastic K calculation |
d |
Smoothing period for D line |
Data.table with Stochastic D values for each symbol
data("sample_prices_weekly") data(sample_prices_weekly) data("sample_prices_weekly") stoch_d <- calc_stochastic_d(sample_prices_weekly, k = 14, d = 3) head(stoch_d)data("sample_prices_weekly") data(sample_prices_weekly) data("sample_prices_weekly") stoch_d <- calc_stochastic_d(sample_prices_weekly, k = 14, d = 3) head(stoch_d)
Computes Stochastic RSI (%K) per column over a rolling window, returning
values in [0, 1]. For each symbol, RSI is computed with TTR::RSI() over
rsi_length periods; then StochRSI is
,
where is stoch_length. If the range is zero the value is handled
per on_const_window (default "zero").
calc_stochrsi( data, length = 14L, rsi_length = NULL, stoch_length = NULL, on_const_window = c("zero", "na") )calc_stochrsi( data, length = 14L, rsi_length = NULL, stoch_length = NULL, on_const_window = c("zero", "na") )
data |
A |
length |
Integer lookback used when |
rsi_length |
Optional integer RSI lookback. Default: |
stoch_length |
Optional integer stochastic window. Default: |
on_const_window |
How to handle windows where |
A data.table with Date and symbol columns containing StochRSI
in [0, 1], with leading NAs for warmup.
TTR::RSI(), calc_momentum(), calc_moving_average(),
filter_top_n(), weight_by_risk_parity()
data(sample_prices_weekly) s <- calc_stochrsi(sample_prices_weekly, length = 14) head(s)data(sample_prices_weekly) s <- calc_stochrsi(sample_prices_weekly, length = 14) head(s)
Carries portfolio positions (from a weekly or lower-frequency backtest) forward to daily dates, multiplies by daily prices, and combines with cash to produce a daily portfolio value series for monitoring and analytics.
calculate_daily_values( positions, daily_prices, strategy_dates, initial_capital, cash_values )calculate_daily_values( positions, daily_prices, strategy_dates, initial_capital, cash_values )
positions |
A |
daily_prices |
A |
strategy_dates |
A |
initial_capital |
Numeric scalar. Starting cash used for days before
the first position exists (typically |
cash_values |
Optional numeric vector of cash balances at the strategy
dates (e.g., |
A list with components:
dates Daily dates within the strategy span.
portfolio_values Daily total portfolio value (positions + cash).
positions_value Daily mark-to-market of positions only.
cash Daily carried cash series.
# Minimal end-to-end example using bundled data and a simple weekly backtest library(PortfolioTesteR) data(sample_prices_weekly); data(sample_prices_daily) # Build a tiny strategy: momentum -> top-3 -> equal weights mom <- calc_momentum(sample_prices_weekly, lookback = 12) sel <- filter_top_n(mom, n = 3) W <- weight_equally(sel) bt <- run_backtest(sample_prices_weekly, W, name = "Demo") # Compute daily monitoring values from positions + cash vals <- calculate_daily_values( positions = bt$positions, daily_prices = sample_prices_daily, strategy_dates = bt$dates, initial_capital = bt$initial_capital, cash_values = bt$cash ) # Quick sanity checks head(vals$dates) head(vals$portfolio_values)# Minimal end-to-end example using bundled data and a simple weekly backtest library(PortfolioTesteR) data(sample_prices_weekly); data(sample_prices_daily) # Build a tiny strategy: momentum -> top-3 -> equal weights mom <- calc_momentum(sample_prices_weekly, lookback = 12) sel <- filter_top_n(mom, n = 3) W <- weight_equally(sel) bt <- run_backtest(sample_prices_weekly, W, name = "Demo") # Compute daily monitoring values from positions + cash vals <- calculate_daily_values( positions = bt$positions, daily_prices = sample_prices_daily, strategy_dates = bt$dates, initial_capital = bt$initial_capital, cash_values = bt$cash ) # Quick sanity checks head(vals$dates) head(vals$portfolio_values)
Computes drawdown series from portfolio values.
calculate_drawdown_series(values)calculate_drawdown_series(values)
values |
Numeric vector of portfolio values |
Numeric vector of drawdowns (as negative percentages)
data("sample_prices_weekly") momentum <- calc_momentum(sample_prices_weekly, lookback = 12) sel <- filter_top_n(momentum, n = 10) W <- weight_equally(sel) res <- run_backtest(sample_prices_weekly, W) dd_series <- calculate_drawdown_series(res$portfolio_values) dd_stats <- analyze_drawdowns(dd_series, res$returns)data("sample_prices_weekly") momentum <- calc_momentum(sample_prices_weekly, lookback = 12) sel <- filter_top_n(momentum, n = 10) W <- weight_equally(sel) res <- run_backtest(sample_prices_weekly, W) dd_series <- calculate_drawdown_series(res$portfolio_values) dd_stats <- analyze_drawdowns(dd_series, res$returns)
Row-wise caps on a wide weights table (Date + symbols):
per-symbol cap (max_per_symbol)
optional per-group cap (max_per_group) using group_map
cap_exposure( weights, max_per_symbol = NULL, group_map = NULL, max_per_group = NULL, allow_short = FALSE, renormalize = c("none", "down", "both"), renormalize_down = FALSE, renorm = NULL, cash_col = NULL, caps = NULL ) cap_exposure( weights, max_per_symbol = NULL, group_map = NULL, max_per_group = NULL, allow_short = FALSE, renormalize = c("none", "down", "both"), renormalize_down = FALSE, renorm = NULL, cash_col = NULL, caps = NULL )cap_exposure( weights, max_per_symbol = NULL, group_map = NULL, max_per_group = NULL, allow_short = FALSE, renormalize = c("none", "down", "both"), renormalize_down = FALSE, renorm = NULL, cash_col = NULL, caps = NULL ) cap_exposure( weights, max_per_symbol = NULL, group_map = NULL, max_per_group = NULL, allow_short = FALSE, renormalize = c("none", "down", "both"), renormalize_down = FALSE, renorm = NULL, cash_col = NULL, caps = NULL )
weights |
data.frame/data.table: columns |
max_per_symbol |
numeric scalar or named vector (absolute cap per symbol). |
group_map |
optional named character vector or data.frame( |
max_per_group |
optional numeric scalar (per-group gross cap). |
allow_short |
logical; if |
renormalize |
character: one of |
renormalize_down |
logical; deprecated alias for down-only (kept for compatibility). |
renorm |
logical; deprecated alias; if |
cash_col |
optional character; if provided, set to |
caps |
optional list; alternative to split args (see details above). |
Negatives are clamped to 0 unless allow_short = TRUE.
Renormalization options:
renormalize = "none": leave gross (sum of positive weights) as is.
renormalize = "down": if gross > 1, scale down so gross == 1.
renormalize = "both": if 0 < gross != 1, scale so gross == 1.
The argument renorm (logical) is a deprecated alias;
TRUE behaves like renormalize = "both".
The caps list form is supported:
Maximum absolute weight per symbol (0-1).
Maximum gross weight per group (0-1).
Named character vector mapping symbol -> group.
data.table of capped (and optionally renormalized) weights.
Cap turnover sequentially across dates
cap_turnover(weights, max_turnover = 0.2)cap_turnover(weights, max_turnover = 0.2)
weights |
desired weight panel. |
max_turnover |
maximum per-rebalance turnover (0..1). |
executed weights after turnover capping.
Carry-forward weights between rebalances (validation helper)
carry_forward_weights(weights)carry_forward_weights(weights)
weights |
Wide weight panel (Date + symbols) with weights only on rebalance rows. |
weight panel with rows filled forward and each active row sum=1.
Combines multiple filter conditions using AND or OR logic.
combine_filters(..., op = "and", apply_when = NULL, debug = FALSE)combine_filters(..., op = "and", apply_when = NULL, debug = FALSE)
... |
Two or more filter data frames to combine |
op |
Operation: "and" or "or" |
apply_when |
Optional condition vector for conditional filtering |
debug |
Print debug information (default: FALSE) |
Combined binary selection matrix
data("sample_prices_weekly") momentum <- calc_momentum(sample_prices_weekly, 12) rsi <- calc_rsi(sample_prices_weekly, 14) # Create individual filters high_momentum <- filter_above(momentum, 0.05) moderate_rsi <- filter_between(rsi, 40, 60) # Combine them combined <- combine_filters(high_momentum, moderate_rsi, op = "and")data("sample_prices_weekly") momentum <- calc_momentum(sample_prices_weekly, 12) rsi <- calc_rsi(sample_prices_weekly, 14) # Create individual filters high_momentum <- filter_above(momentum, 0.05) moderate_rsi <- filter_between(rsi, 40, 60) # Combine them combined <- combine_filters(high_momentum, moderate_rsi, op = "and")
Combine several wide score panels (Date + symbols) into a single panel
by applying one of several aggregation methods.
combine_scores( scores_list, method = c("mean", "weighted", "rank_avg", "trimmed_mean"), weights = NULL, trim = 0.1 )combine_scores( scores_list, method = c("mean", "weighted", "rank_avg", "trimmed_mean"), weights = NULL, trim = 0.1 )
scores_list |
List of wide score panels to combine (each has columns |
method |
Character, one of |
weights |
Optional numeric vector of length equal to |
trim |
Numeric in |
method = "mean": simple column-wise mean across panels.
method = "weighted": weighted mean; see weights.
method = "rank_avg": average of within-date normalized ranks.
method = "trimmed_mean": mean with trim fraction removed at both tails.
A data.table with columns Date + symbols, containing the combined scores.
Blends multiple weight matrices with specified weights. Useful for multi-factor strategies that combine different allocation approaches. Optimized using matrix operations for 1000x+ speedup.
combine_weights(weight_matrices, weights = NULL)combine_weights(weight_matrices, weights = NULL)
weight_matrices |
List of weight data frames to combine |
weights |
Numeric vector of weights for each matrix (default: equal) |
Data.table with blended portfolio weights
data("sample_prices_weekly") # Calculate signals momentum <- calc_momentum(sample_prices_weekly, lookback = 12) selected <- filter_top_n(momentum, n = 10) volatility <- calc_rolling_volatility(sample_prices_weekly, lookback = 20) # Combine momentum and low-vol weights mom_weights <- weight_by_signal(selected, momentum) vol_weights <- weight_by_signal(selected, invert_signal(volatility)) combined <- combine_weights(list(mom_weights, vol_weights), weights = c(0.7, 0.3))data("sample_prices_weekly") # Calculate signals momentum <- calc_momentum(sample_prices_weekly, lookback = 12) selected <- filter_top_n(momentum, n = 10) volatility <- calc_rolling_volatility(sample_prices_weekly, lookback = 20) # Combine momentum and low-vol weights mom_weights <- weight_by_signal(selected, momentum) vol_weights <- weight_by_signal(selected, invert_signal(volatility)) combined <- combine_weights(list(mom_weights, vol_weights), weights = c(0.7, 0.3))
Resamples daily or weekly data to n-week periods. Handles week-ending calculations and various aggregation methods.
convert_to_nweeks(data, n = 1, method = "last")convert_to_nweeks(data, n = 1, method = "last")
data |
Data.table with Date column and price columns |
n |
Number of weeks to aggregate (default: 1 for weekly) |
method |
Aggregation method: "last" or "mean" (default: "last") |
Data.table resampled to n-week frequency
data("sample_prices_daily") # Convert daily to weekly weekly <- convert_to_nweeks(sample_prices_daily, n = 1) # Convert to bi-weekly biweekly <- convert_to_nweeks(sample_prices_daily, n = 2)data("sample_prices_daily") # Convert daily to weekly weekly <- convert_to_nweeks(sample_prices_daily, n = 1) # Convert to bi-weekly biweekly <- convert_to_nweeks(sample_prices_daily, n = 2)
Count finite entries per date
coverage_by_date(panel)coverage_by_date(panel)
panel |
wide panel |
data.table with Date, n_finite.
Transforms continuous indicators into discrete regime categories.
create_regime_buckets( indicator, breakpoints, labels = NULL, use_percentiles = FALSE )create_regime_buckets( indicator, breakpoints, labels = NULL, use_percentiles = FALSE )
indicator |
Numeric vector or data frame with indicator values |
breakpoints |
Numeric vector of breakpoints |
labels |
Optional character vector of regime names |
use_percentiles |
Use percentiles instead of fixed breakpoints (default: FALSE) |
Integer vector of regime classifications
data("sample_prices_weekly") momentum <- calc_momentum(sample_prices_weekly, lookback = 12) selected <- filter_top_n(momentum, 10) # Create VIX-like indicator from volatility vol <- calc_rolling_volatility(sample_prices_weekly, lookback = 20) vix_proxy <- vol$SPY * 100 # Scale to VIX-like values regimes <- create_regime_buckets(vix_proxy, c(15, 25))data("sample_prices_weekly") momentum <- calc_momentum(sample_prices_weekly, lookback = 12) selected <- filter_top_n(momentum, 10) # Create VIX-like indicator from volatility vol <- calc_rolling_volatility(sample_prices_weekly, lookback = 20) vix_proxy <- vol$SPY * 100 # Scale to VIX-like values regimes <- create_regime_buckets(vix_proxy, c(15, 25))
Reads stock price data from CSV files with flexible column naming. Automatically standardizes to library format.
csv_adapter( file_path, date_col = "Date", symbol_col = "Symbol", price_col = "Price", frequency = "daily", symbol_order = NULL )csv_adapter( file_path, date_col = "Date", symbol_col = "Symbol", price_col = "Price", frequency = "daily", symbol_order = NULL )
file_path |
Path to CSV file |
date_col |
Name of date column (default: "date") |
symbol_col |
Name of symbol column (default: "symbol") |
price_col |
Name of price column (default: "close") |
frequency |
Target frequency: "daily" or "weekly" (default: "daily") |
symbol_order |
Optional vector to order symbols |
Data.table with Date column and price columns
# Create a temporary tidy CSV from included weekly sample data (offline, fast) data("sample_prices_weekly") PW <- as.data.frame(sample_prices_weekly) syms <- setdiff(names(PW), "Date")[1:2] stk <- stack(PW[1:10, syms]) tidy <- data.frame( Date = rep(PW$Date[1:10], times = length(syms)), Symbol = stk$ind, Price = stk$values ) tmp <- tempfile(fileext = ".csv") write.csv(tidy, tmp, row.names = FALSE) prices <- csv_adapter(tmp) head(prices) unlink(tmp)# Create a temporary tidy CSV from included weekly sample data (offline, fast) data("sample_prices_weekly") PW <- as.data.frame(sample_prices_weekly) syms <- setdiff(names(PW), "Date")[1:2] stk <- stack(PW[1:10, syms]) tidy <- data.frame( Date = rep(PW$Date[1:10], times = length(syms)), Symbol = stk$ind, Price = stk$values ) tmp <- tempfile(fileext = ".csv") write.csv(tidy, tmp, row.names = FALSE) prices <- csv_adapter(tmp) head(prices) unlink(tmp)
Purged/embargoed K-fold CV for sequence models (inside IS window)
cv_tune_seq( features_list, labels, is_start, is_end, steps_grid = c(12L, 26L, 52L), horizon = 4L, fit_fn, predict_fn, k = 3L, purge = NULL, embargo = NULL, group = c("pooled"), max_train_samples = NULL, max_val_samples = NULL, na_action = c("omit", "zero"), metric = c("spearman", "rmse") )cv_tune_seq( features_list, labels, is_start, is_end, steps_grid = c(12L, 26L, 52L), horizon = 4L, fit_fn, predict_fn, k = 3L, purge = NULL, embargo = NULL, group = c("pooled"), max_train_samples = NULL, max_val_samples = NULL, na_action = c("omit", "zero"), metric = c("spearman", "rmse") )
features_list |
List of feature panels (Date + symbols) for sequences. |
labels |
Label panel aligned to features (Date + symbols). |
is_start, is_end
|
Inclusive IS window indices (on aligned dates). |
steps_grid |
Integer vector of candidate sequence lengths. |
horizon |
Forecast horizon (periods ahead). |
fit_fn |
Function (X, y) -> model for sequences. |
predict_fn |
Function (model, Xnew) -> numeric scores. |
k |
Number of CV folds. |
purge |
Gap (obs) removed between train/val within folds (default uses steps). |
embargo |
Embargo (obs) after validation to avoid bleed (default uses horizon). |
group |
'pooled', 'per_symbol', or 'per_group'. |
max_train_samples |
Optional cap on IS samples per fold. |
max_val_samples |
Optional cap on validation samples per fold. |
na_action |
How to handle NA features ('omit' or 'zero'). |
metric |
IC metric ('spearman' or 'pearson'). |
data.table with columns like steps, folds, and CV score.
Demo sector (group) map for examples/tests
demo_sector_map(symbols, n_groups = 2L)demo_sector_map(symbols, n_groups = 2L)
symbols |
character vector of tickers. |
n_groups |
integer number of groups to assign (cycled). |
A data.table with columns Symbol and Group (title-case), for demo use.
# Minimal usage syms <- c("AAPL","MSFT","AMZN","XOM","JPM") gdf <- demo_sector_map(syms, n_groups = 3L) # columns: Symbol, Group print(gdf) # Use with cap_exposure(): convert to a named vector (names = symbols) gmap <- stats::setNames(gdf$Group, gdf$Symbol) data(sample_prices_weekly) mom12 <- calc_momentum(sample_prices_weekly, 12) sel10 <- filter_top_n(mom12, 10) w_eq <- weight_equally(sel10) w_cap <- cap_exposure( weights = w_eq, max_per_symbol = 0.10, group_map = gmap, # <- named vector, OK for current cap_exposure() max_per_group = 0.45, renormalize_down = TRUE ) head(w_cap)# Minimal usage syms <- c("AAPL","MSFT","AMZN","XOM","JPM") gdf <- demo_sector_map(syms, n_groups = 3L) # columns: Symbol, Group print(gdf) # Use with cap_exposure(): convert to a named vector (names = symbols) gmap <- stats::setNames(gdf$Group, gdf$Symbol) data(sample_prices_weekly) mom12 <- calc_momentum(sample_prices_weekly, 12) sel10 <- filter_top_n(mom12, 10) w_eq <- weight_equally(sel10) w_cap <- cap_exposure( weights = w_eq, max_per_symbol = 0.10, group_map = gmap, # <- named vector, OK for current cap_exposure() max_per_group = 0.45, renormalize_down = TRUE ) head(w_cap)
Scrapes current S&P 500 constituent list with sector classifications from Wikipedia and returns as a data.table.
download_sp500_sectors()download_sp500_sectors()
Data.table with columns: Symbol, Security, Sector, SubIndustry, Industry
sectors <- download_sp500_sectors() head(sectors)sectors <- download_sp500_sectors() head(sectors)
Converts input to data.table if needed, always returning a copy to prevent accidental data mutation. Core safety function used throughout the library.
ensure_dt_copy(data)ensure_dt_copy(data)
data |
Data.frame or data.table |
Copy of data as data.table
data("sample_prices_weekly") dt <- ensure_dt_copy(sample_prices_weekly) # Safe to modify dtdata("sample_prices_weekly") dt <- ensure_dt_copy(sample_prices_weekly) # Safe to modify dt
Evaluate scores vs labels (IC and hit-rate)
evaluate_scores( scores, labels, top_frac = 0.2, method = c("spearman", "pearson") )evaluate_scores( scores, labels, top_frac = 0.2, method = c("spearman", "pearson") )
scores |
Wide score panel. |
labels |
Wide label panel aligned to scores. |
top_frac |
fraction in the top bucket for hit-rate. |
method |
|
data.table with Date, IC, hit_rate; ICIR on attr(result,"ICIR").
Convenience function to select stocks with signal above a value.
filter_above(signal_df, value)filter_above(signal_df, value)
signal_df |
Data frame with signal values |
value |
Threshold value |
Binary selection matrix
data("sample_prices_weekly") rsi <- calc_rsi(sample_prices_weekly, 14) high_rsi <- filter_above(rsi, 70)data("sample_prices_weekly") rsi <- calc_rsi(sample_prices_weekly, 14) high_rsi <- filter_above(rsi, 70)
Convenience function to select stocks with signal below a value.
filter_below(signal_df, value)filter_below(signal_df, value)
signal_df |
Data frame with signal values |
value |
Threshold value |
Binary selection matrix
data("sample_prices_weekly") rsi <- calc_rsi(sample_prices_weekly, 14) oversold <- filter_below(rsi, 30)data("sample_prices_weekly") rsi <- calc_rsi(sample_prices_weekly, 14) oversold <- filter_below(rsi, 30)
Selects stocks with signal values between lower and upper bounds.
filter_between(signal_df, lower, upper)filter_between(signal_df, lower, upper)
signal_df |
Data frame with signal values |
lower |
Lower bound (inclusive) |
upper |
Upper bound (inclusive) |
Binary selection matrix
data("sample_prices_weekly") rsi <- calc_rsi(sample_prices_weekly, 14) # Select stocks with RSI between 30 and 70 neutral_rsi <- filter_between(rsi, 30, 70)data("sample_prices_weekly") rsi <- calc_rsi(sample_prices_weekly, 14) # Select stocks with RSI between 30 and 70 neutral_rsi <- filter_between(rsi, 30, 70)
Select securities in the top or bottom X percentile. More intuitive than filter_top_n when universe size varies.
filter_by_percentile(signal_df, percentile, type = c("top", "bottom"))filter_by_percentile(signal_df, percentile, type = c("top", "bottom"))
signal_df |
DataFrame with signal values |
percentile |
Percentile threshold (0-100) |
type |
"top" for highest signals, "bottom" for lowest |
Binary selection matrix
data("sample_prices_weekly") momentum <- calc_momentum(sample_prices_weekly, 12) # Select top 20th percentile top_20pct <- filter_by_percentile(momentum, 20, type = "top")data("sample_prices_weekly") momentum <- calc_momentum(sample_prices_weekly, 12) # Select top 20th percentile top_20pct <- filter_by_percentile(momentum, 20, type = "top")
Selects the top N (best) or worst N stocks based on signal strength. Optimized using matrix operations for 5-10x speedup.
filter_rank(signal_df, n, type = c("top", "worst"))filter_rank(signal_df, n, type = c("top", "worst"))
signal_df |
Data frame with Date column and signal values |
n |
Number of stocks to select |
type |
"top" for highest values, "worst" for lowest values |
Binary selection matrix (1 = selected, 0 = not selected)
data("sample_prices_weekly") momentum <- calc_momentum(sample_prices_weekly, 12) # Select 10 highest momentum stocks top10 <- filter_rank(momentum, 10, type = "top")data("sample_prices_weekly") momentum <- calc_momentum(sample_prices_weekly, 12) # Select 10 highest momentum stocks top10 <- filter_rank(momentum, 10, type = "top")
Selects stocks above or below a threshold value.
filter_threshold(signal_df, value, type = c("above", "below"))filter_threshold(signal_df, value, type = c("above", "below"))
signal_df |
Data frame with signal values |
value |
Threshold value |
type |
"above" or "below" |
Binary selection matrix
data("sample_prices_weekly") momentum <- calc_momentum(sample_prices_weekly, 12) # Select stocks with positive momentum positive <- filter_threshold(momentum, 0, type = "above")data("sample_prices_weekly") momentum <- calc_momentum(sample_prices_weekly, 12) # Select stocks with positive momentum positive <- filter_threshold(momentum, 0, type = "above")
Most commonly used filter function. Selects top N (highest) or bottom N (lowest) stocks by signal value. Optimized for 5-10x faster performance.
filter_top_n(signal_df, n, ascending = FALSE)filter_top_n(signal_df, n, ascending = FALSE)
signal_df |
Data frame with Date column and signal values |
n |
Number of stocks to select |
ascending |
FALSE (default) selects highest, TRUE selects lowest |
Binary selection matrix (1 = selected, 0 = not selected)
data("sample_prices_weekly") momentum <- calc_momentum(sample_prices_weekly, 12) # Select 10 highest momentum stocks top_momentum <- filter_top_n(momentum, n = 10)data("sample_prices_weekly") momentum <- calc_momentum(sample_prices_weekly, 12) # Select 10 highest momentum stocks top_momentum <- filter_top_n(momentum, n = 10)
Selects top N stocks by signal, but only from those meeting a condition. Combines qualification and ranking in one step.
filter_top_n_where( signal_df, n, condition_df, min_qualified = 1, ascending = FALSE )filter_top_n_where( signal_df, n, condition_df, min_qualified = 1, ascending = FALSE )
signal_df |
Signal values for ranking |
n |
Number to select |
condition_df |
Binary matrix of qualified stocks |
min_qualified |
Minimum qualified stocks required (default: 1) |
ascending |
FALSE for highest, TRUE for lowest |
Binary selection matrix
data("sample_prices_weekly") # Calculate indicators momentum <- calc_momentum(sample_prices_weekly, 12) ma20 <- calc_moving_average(sample_prices_weekly, 20) distance_from_ma <- calc_distance(sample_prices_weekly, ma20) # Top 10 momentum stocks from those above MA above_ma <- filter_above(distance_from_ma, 0) top_qualified <- filter_top_n_where(momentum, 10, above_ma)data("sample_prices_weekly") # Calculate indicators momentum <- calc_momentum(sample_prices_weekly, 12) ma20 <- calc_moving_average(sample_prices_weekly, 20) distance_from_ma <- calc_distance(sample_prices_weekly, ma20) # Top 10 momentum stocks from those above MA above_ma <- filter_above(distance_from_ma, 0) top_qualified <- filter_top_n_where(momentum, 10, above_ma)
Automatically detects whether data is daily, weekly, monthly, or quarterly based on date spacing.
get_data_frequency(dates)get_data_frequency(dates)
dates |
Vector of Date objects |
Character string: "daily", "weekly", "monthly", or "quarterly"
data("sample_prices_weekly") freq <- get_data_frequency(sample_prices_weekly$Date)data("sample_prices_weekly") freq <- get_data_frequency(sample_prices_weekly$Date)
Information Coefficient time series
ic_series(scores, labels, method = c("spearman", "pearson"))ic_series(scores, labels, method = c("spearman", "pearson"))
scores |
Wide score panel (Date + symbols). |
labels |
Wide label panel aligned to scores. |
method |
IC method ('spearman' or 'pearson'). |
data.table with Date and IC.
Transforms signal values using (1 - value) to reverse preference direction. Useful when high values indicate something to avoid. For example, inverting volatility makes low-vol stocks appear as high signals.
invert_signal(signal_df)invert_signal(signal_df)
signal_df |
Data frame with Date column and signal columns |
Data frame with inverted signal values
data("sample_prices_weekly") # Prefer low volatility stocks volatility <- calc_rolling_volatility(sample_prices_weekly, 20) stability_signal <- invert_signal(volatility) # Select top 10 momentum stocks first momentum <- calc_momentum(sample_prices_weekly, 12) selected <- filter_top_n(momentum, 10) # Weight by inverted volatility (low vol = high weight) weights <- weight_by_signal(selected, stability_signal)data("sample_prices_weekly") # Prefer low volatility stocks volatility <- calc_rolling_volatility(sample_prices_weekly, 20) stability_signal <- invert_signal(volatility) # Select top 10 momentum stocks first momentum <- calc_momentum(sample_prices_weekly, 12) selected <- filter_top_n(momentum, 10) # Weight by inverted volatility (low vol = high weight) weights <- weight_by_signal(selected, stability_signal)
Align and join a list of wide panels on their common dates. Each input
panel must have one Date column and a disjoint set of symbol columns.
join_panels(panels)join_panels(panels)
panels |
List of wide panels ( |
All panels are first aligned to the intersection of their Date values.
Symbol names must be unique across panels; if the same symbol appears in
multiple inputs, an error is raised.
A data.table with Date plus the union of all symbol columns, restricted to common dates.
Legacy selector used across examples/vignettes. Works on a WIDE table
(Date + one column per symbol) and returns a WIDE logical mask with at
most max_positions TRUE values per row.
limit_positions( selection_df, max_positions, ranking_signal = NULL, verbose = FALSE )limit_positions( selection_df, max_positions, ranking_signal = NULL, verbose = FALSE )
selection_df |
data.frame/data.table with columns: |
max_positions |
positive integer, maximum selections to keep per row. |
ranking_signal |
optional data.frame/data.table, same dims & columns as
|
verbose |
logical; if |
If ranking_signal is supplied, it must have the same shape and columns
as selection_df; the function keeps the top-K (largest) scores among
the currently selected columns in that row. If ranking_signal is NULL,
it falls back to the values in selection_df (if numeric), otherwise keeps
the first K selected symbols in column order (deterministic).
A data.table with the same columns as selection_df, where symbol
columns are logical and at most max_positions are TRUE in each row.
data(sample_prices_weekly) mom12 <- PortfolioTesteR::calc_momentum(sample_prices_weekly, 12) # WIDE numeric sel10 <- PortfolioTesteR::filter_top_n(mom12, 10) # WIDE logical/numeric # Ensure logical mask syms <- setdiff(names(sel10), "Date") sel_mask <- data.table::as.data.table(sel10) sel_mask[, (syms) := lapply(.SD, function(z) as.logical(as.integer(z))), .SDcols = syms] # Keep at most 10 per date using momentum as the ranking signal lim <- limit_positions(selection_df = sel_mask, max_positions = 10L, ranking_signal = mom12, verbose = FALSE) head(lim)data(sample_prices_weekly) mom12 <- PortfolioTesteR::calc_momentum(sample_prices_weekly, 12) # WIDE numeric sel10 <- PortfolioTesteR::filter_top_n(mom12, 10) # WIDE logical/numeric # Ensure logical mask syms <- setdiff(names(sel10), "Date") sel_mask <- data.table::as.data.table(sel10) sel_mask[, (syms) := lapply(.SD, function(z) as.logical(as.integer(z))), .SDcols = syms] # Keep at most 10 per date using momentum as the ranking signal lim <- limit_positions(selection_df = sel_mask, max_positions = 10L, ranking_signal = mom12, verbose = FALSE) head(lim)
Shows all example scripts included with the PortfolioTesteR package. These examples demonstrate various strategy patterns and library functions.
list_examples()list_examples()
Character vector of example filenames
# See available examples list_examples() # Run a specific example # run_example("example_momentum_basic.R")# See available examples list_examples() # Run a specific example # run_example("example_momentum_basic.R")
Handles loading regular stocks and VIX together, with VIX loaded separately without auto-update to avoid issues.
load_mixed_symbols( db_path, symbols, start_date, end_date, frequency = "weekly", use_adjusted = TRUE )load_mixed_symbols( db_path, symbols, start_date, end_date, frequency = "weekly", use_adjusted = TRUE )
db_path |
Path to SQLite database |
symbols |
Character vector including regular stocks and optionally "VIX" |
start_date |
Start date for data |
end_date |
End date for data |
frequency |
Data frequency (default: "weekly") |
use_adjusted |
Use adjusted prices (default: TRUE) |
data.table with all symbols properly loaded
mixed <- load_mixed_symbols( db_path = "sp500.db", symbols = c("AAPL", "MSFT", "VIX"), start_date = "2020-01-01", end_date = "2020-12-31", frequency = "weekly" ) head(mixed)mixed <- load_mixed_symbols( db_path = "sp500.db", symbols = c("AAPL", "MSFT", "VIX"), start_date = "2020-01-01", end_date = "2020-12-31", frequency = "weekly" ) head(mixed)
Compute forward returns over a fixed horizon and align them to the decision date.
make_labels(prices, horizon = 4L, type = c("log", "simple", "sign"))make_labels(prices, horizon = 4L, type = c("log", "simple", "sign"))
prices |
Wide price panel ( |
horizon |
Integer |
type |
Character, one of |
For each date , the label is computed from prices at and .
type = "simple":
type = "log":
type = "sign": sign(simple return)
Trailing dates that do not have horizon steps ahead are set to NA.
A data.table with Date + symbols containing the labels.
Simple adapter for when users provide their own data frame. Ensures proper Date formatting and sorting.
manual_adapter(data, date_col = "Date")manual_adapter(data, date_col = "Date")
data |
User-provided data frame |
date_col |
Name of date column (default: "Date") |
Standardized data.table
# Use your own data frame data("sample_prices_weekly") my_prices <- manual_adapter(sample_prices_weekly)# Use your own data frame data("sample_prices_weekly") my_prices <- manual_adapter(sample_prices_weekly)
Membership stability across dates
membership_stability(mask)membership_stability(mask)
mask |
logical mask panel from selection. |
data.table with Date, stability (Jaccard vs previous date).
Calculate Sharpe Ratio with Frequency Detection
metric_sharpe(bt)metric_sharpe(bt)
bt |
Backtest result object with $returns and (optionally) $dates |
Annualized Sharpe ratio
Builds new panels from existing ones via elementwise operations.
Specification accepts either a shorthand list(new = c("A","B")) (defaults to product),
or a structured form list(new = list(panels=c("A","B","C"), op=/, how="intersect", fill=NA)).
ml_add_interactions(features, interactions)ml_add_interactions(features, interactions)
features |
List of existing panels (wide data frames with |
interactions |
Named list describing interactions to add. |
The input features list with additional interaction panels.
## Not run: X2 <- ml_add_interactions(X, list(mom_vol = c("mom12","vol"))) ## End(Not run)## Not run: X2 <- ml_add_interactions(X, list(mom_vol = c("mom12","vol"))) ## End(Not run)
One-call backtest wrapper (tabular features)
ml_backtest( features_list, labels, fit_fn, predict_fn, schedule = list(is = 156L, oos = 4L, step = 4L), group = c("pooled", "per_symbol", "per_group"), transform = c("none", "zscore", "rank"), selection = list(top_k = 15L, max_per_group = NULL), group_map = NULL, weighting = list(method = "softmax", temperature = 12, floor = 0), caps = list(max_per_symbol = 0.1, max_per_group = NULL), turnover = NULL, prices, initial_capital = 1e+05, name = "ML backtest" )ml_backtest( features_list, labels, fit_fn, predict_fn, schedule = list(is = 156L, oos = 4L, step = 4L), group = c("pooled", "per_symbol", "per_group"), transform = c("none", "zscore", "rank"), selection = list(top_k = 15L, max_per_group = NULL), group_map = NULL, weighting = list(method = "softmax", temperature = 12, floor = 0), caps = list(max_per_symbol = 0.1, max_per_group = NULL), turnover = NULL, prices, initial_capital = 1e+05, name = "ML backtest" )
features_list |
list of wide panels (each: |
labels |
Wide label panel (Date + symbols). |
fit_fn |
function |
predict_fn |
function |
schedule |
list with elements |
group |
one of |
transform |
|
selection |
list: |
group_map |
optional |
weighting |
list: |
caps |
list: |
turnover |
Optional turnover cap settings (currently advisory/unused). |
prices |
price panel used by the backtester ( |
initial_capital |
starting capital. |
name |
string for the backtest result. |
list: scores, mask, weights, backtest.
data(sample_prices_weekly); data(sample_prices_daily) mom <- panel_lag(calc_momentum(sample_prices_weekly, 12), 1) vol <- panel_lag(align_to_timeframe( calc_rolling_volatility(sample_prices_daily, 20), sample_prices_weekly$Date, "forward_fill"), 1) Y <- make_labels(sample_prices_weekly, 4, "log") fit_lm <- function(X,y){ Xc <- cbind(1,X); list(coef=stats::lm.fit(Xc,y)$coefficients) } pred_lm <- function(m,X){ as.numeric(cbind(1,X) %*% m$coef) } res <- ml_backtest(list(mom=mom, vol=vol), Y, fit_lm, pred_lm, schedule = list(is=52,oos=4,step=4), transform = "zscore", selection = list(top_k=10), weighting = list(method="softmax", temperature=12), caps = list(max_per_symbol=0.10), prices = sample_prices_weekly, initial_capital = 1e5) res$backtestdata(sample_prices_weekly); data(sample_prices_daily) mom <- panel_lag(calc_momentum(sample_prices_weekly, 12), 1) vol <- panel_lag(align_to_timeframe( calc_rolling_volatility(sample_prices_daily, 20), sample_prices_weekly$Date, "forward_fill"), 1) Y <- make_labels(sample_prices_weekly, 4, "log") fit_lm <- function(X,y){ Xc <- cbind(1,X); list(coef=stats::lm.fit(Xc,y)$coefficients) } pred_lm <- function(m,X){ as.numeric(cbind(1,X) %*% m$coef) } res <- ml_backtest(list(mom=mom, vol=vol), Y, fit_lm, pred_lm, schedule = list(is=52,oos=4,step=4), transform = "zscore", selection = list(top_k=10), weighting = list(method="softmax", temperature=12), caps = list(max_per_symbol=0.10), prices = sample_prices_weekly, initial_capital = 1e5) res$backtest
Convenience wrapper around ml_backtest() that repeats the
same specification across multiple horizons, returning a named list of
backtest objects keyed as "H1w", "H4w", "H13w", etc.
ml_backtest_multi( features_list, prices_weekly, horizons, fit_fn, predict_fn, schedule, transform = "zscore", selection = list(top_k = 20L), weighting = list(method = "softmax", temperature = 12), caps = list(max_per_symbol = 0.1), group_mode = c("pooled", "per_group"), group_map = NULL, initial_capital = 1e+05, name_prefix = "", seed = NULL, ... )ml_backtest_multi( features_list, prices_weekly, horizons, fit_fn, predict_fn, schedule, transform = "zscore", selection = list(top_k = 20L), weighting = list(method = "softmax", temperature = 12), caps = list(max_per_symbol = 0.1), group_mode = c("pooled", "per_group"), group_map = NULL, initial_capital = 1e+05, name_prefix = "", seed = NULL, ... )
features_list |
Named list of data.tables with factor scores (each with a
|
prices_weekly |
Wide price table (weekly) with |
horizons |
Integer vector of horizons in weeks (e.g., |
fit_fn, predict_fn
|
Model fit/predict closures as returned by
|
schedule |
Walk-forward schedule list with elements |
transform |
Feature transform (default |
selection |
List describing selection rules (e.g., |
weighting |
List describing weighting rules (e.g., |
caps |
List with exposure caps (e.g., |
group_mode |
|
group_map |
A two-column table with columns |
initial_capital |
Numeric. Starting capital for the backtest (default |
name_prefix |
Optional string prefixed to each backtest title. |
seed |
Optional integer. If provided, the same seed is set before each horizon’s backtest call to ensure deterministic tie-breaks. |
... |
Additional arguments forwarded to |
This function does not change core behavior; it only removes boilerplate when running identical specs across horizons and (optionally) grouping regimes. It preserves all defaults you pass for selection, weighting, transforms, caps, and schedule.
A named list of backtest objects (as returned by
ml_backtest()), with names like "H1w", "H4w", … .
library(PortfolioTesteR) data(sample_prices_weekly, package = "PortfolioTesteR") # Minimal features for the example X <- ml_prepare_features( prices_weekly = sample_prices_weekly, include = c("mom12","mom26") ) # Simple deterministic model model <- ml_make_model("linear") sched <- list(is = 156L, oos = 4L, step = 4L) set.seed(42) bt_list <- ml_backtest_multi( features_list = X, prices_weekly = sample_prices_weekly, horizons = c(1L, 4L), fit_fn = model$fit, predict_fn = model$predict, schedule = sched, selection = list(top_k = 5L), weighting = list(method = "softmax", temperature = 12), caps = list(max_per_symbol = 0.10), group_mode = "pooled", name_prefix = "Demo ", seed = 42 ) names(bt_list) # "H1w" "H4w"library(PortfolioTesteR) data(sample_prices_weekly, package = "PortfolioTesteR") # Minimal features for the example X <- ml_prepare_features( prices_weekly = sample_prices_weekly, include = c("mom12","mom26") ) # Simple deterministic model model <- ml_make_model("linear") sched <- list(is = 156L, oos = 4L, step = 4L) set.seed(42) bt_list <- ml_backtest_multi( features_list = X, prices_weekly = sample_prices_weekly, horizons = c(1L, 4L), fit_fn = model$fit, predict_fn = model$predict, schedule = sched, selection = list(top_k = 5L), weighting = list(method = "softmax", temperature = 12), caps = list(max_per_symbol = 0.10), group_mode = "pooled", name_prefix = "Demo ", seed = 42 ) names(bt_list) # "H1w" "H4w"
One-call backtest wrapper (sequence features)
ml_backtest_seq( features_list, labels, steps = 26L, horizon = 4L, fit_fn, predict_fn, schedule = list(is = 156L, oos = 4L, step = 4L), group = c("pooled", "per_symbol", "per_group"), group_map = NULL, normalize = c("none", "zscore", "minmax"), selection = list(top_k = 15L, max_per_group = NULL), weighting = list(method = "softmax", temperature = 12, floor = 0), caps = list(max_per_symbol = 0.1, max_per_group = NULL), prices, initial_capital = 1e+05, name = "SEQ backtest" )ml_backtest_seq( features_list, labels, steps = 26L, horizon = 4L, fit_fn, predict_fn, schedule = list(is = 156L, oos = 4L, step = 4L), group = c("pooled", "per_symbol", "per_group"), group_map = NULL, normalize = c("none", "zscore", "minmax"), selection = list(top_k = 15L, max_per_group = NULL), weighting = list(method = "softmax", temperature = 12, floor = 0), caps = list(max_per_symbol = 0.1, max_per_group = NULL), prices, initial_capital = 1e+05, name = "SEQ backtest" )
features_list |
list of panels to be stacked over |
labels |
future-return panel aligned to the features. |
steps |
int; lookback length (e.g., 26). |
horizon |
int; label horizon (e.g., 4). |
fit_fn |
function |
predict_fn |
function |
schedule |
list with elements |
group |
one of |
group_map |
optional |
normalize |
|
selection |
list: |
weighting |
list: |
caps |
list: |
prices |
price panel used by the backtester ( |
initial_capital |
starting capital. |
name |
string for the backtest result. |
list: scores, mask, weights, backtest.
# as above, but with steps/horizon and normalize# as above, but with steps/horizon and normalize
Wrapper around ic_series() that first filters scores to formation dates
using an internal filter that keeps rows where at least one score is finite.
ml_ic_series_on_scores(scores_dt, labels_dt, method = "spearman")ml_ic_series_on_scores(scores_dt, labels_dt, method = "spearman")
scores_dt |
Scores table (wide). |
labels_dt |
Labels table (wide). |
method |
Correlation method; |
A data frame/data.table with Date and IC values.
## Not run: ic <- ml_ic_series_on_scores(res_xgb$scores, Y, method = "spearman") ## End(Not run)## Not run: ic <- ml_ic_series_on_scores(res_xgb$scores, Y, method = "spearman") ## End(Not run)
Creates an equal- or user-weighted blend of multiple model objects produced
by ml_make_model(). The returned fit trains each component; predict
combines component predictions with an NA-safe weighted average.
ml_make_ensemble(..., weights = NULL)ml_make_ensemble(..., weights = NULL)
... |
Two or more model objects each with |
weights |
Optional numeric vector of blend weights (recycled). |
A list with $fit and $predict closures for the ensemble.
## Not run: ens <- ml_make_ensemble(ml_make_model("ridge"), ml_make_model("rf"), ml_make_model("xgboost")) ## End(Not run)## Not run: ens <- ml_make_ensemble(ml_make_model("ridge"), ml_make_model("rf"), ml_make_model("xgboost")) ## End(Not run)
Returns a pair of closures fit(X,y) / predict(model, X) implementing
a chosen learner. Implementations are NA-aware and conservative:
glmnet ridge drops rows with any non-finite input; ranger and xgboost
keep NA in X as missing; the linear baseline uses lm.fit.
ml_make_model( type = c("ridge", "rf", "xgboost", "linear"), params = list(), nrounds = 200L, ... )ml_make_model( type = c("ridge", "rf", "xgboost", "linear"), params = list(), nrounds = 200L, ... )
type |
One of |
params |
List of model parameters (passed to backend; used by xgboost). |
nrounds |
Integer boosting rounds (xgboost). |
... |
Additional arguments forwarded to the backend. |
Optional dependencies: glmnet (ridge), ranger (rf), xgboost (xgboost).
If a backend is not available, use "linear" or install the package.
A list with functions fit and predict.
## Not run: ridge <- ml_make_model("ridge") m <- ridge$fit(X_is, y_is) s <- ridge$predict(m, X_oos) ## End(Not run)## Not run: ridge <- ml_make_model("ridge") m <- ridge$fit(X_is, y_is) s <- ridge$predict(m, X_oos) ## End(Not run)
Returns fit/predict closures for sequence models that consume flattened
tabular inputs (n (steps p)) and internally reshape to (n, steps, p).
If Keras/TensorFlow is unavailable, falls back to a linear baseline so
examples remain runnable on CPU-only machines.
ml_make_seq_model( type = c("linear", "gru", "lstm", "cnn1d"), steps = 26L, units = 16L, dense = NULL, dropout = 0.1, epochs = 12L, batch_size = 128L, lr = 0.01, patience = 2L, seed = 123L, deterministic = TRUE, pred_batch_size = 256L )ml_make_seq_model( type = c("linear", "gru", "lstm", "cnn1d"), steps = 26L, units = 16L, dense = NULL, dropout = 0.1, epochs = 12L, batch_size = 128L, lr = 0.01, patience = 2L, seed = 123L, deterministic = TRUE, pred_batch_size = 256L )
type |
One of |
steps |
Integer sequence length (e.g., 26 for 6 months of weeks). |
units |
Hidden units for GRU/LSTM or filters for CNN1D. |
dense |
Optional integer vector of additional dense layers. |
dropout |
Dropout rate for recurrent/CNN core. |
epochs, batch_size
|
Training settings. |
lr |
Learning rate. |
patience |
Early-stopping patience. |
seed |
Integer seed. |
deterministic |
Logical; set determinism knobs when TRUE. |
pred_batch_size |
Fixed batch size used at prediction time. |
Determinism knobs: fixed seeds, TF_DETERMINISTIC_OPS=1, no shuffle,
workers=1, and a fixed pred_batch_size to minimise retracing.
Optional dependencies: keras and tensorflow. When not available,
the factory returns the linear fallback.
A list with $fit and $predict closures.
## Not run: seq_gru <- ml_make_seq_model("gru", steps = 26, units = 16, epochs = 12) ## End(Not run)## Not run: seq_gru <- ml_make_seq_model("gru", steps = 26, units = 16, epochs = 12) ## End(Not run)
Applies an elementwise binary operator to two date-aligned wide panels
(first column Date, other columns are symbols), preserving the Date
column and a consistent symbol set. Supports intersection or union of
column sets; missing entries introduced by how="union" are filled.
ml_panel_op(A, B, op = `*`, how = c("intersect", "union"), fill = NA_real_)ml_panel_op(A, B, op = `*`, how = c("intersect", "union"), fill = NA_real_)
A, B
|
Data frames with a |
op |
Binary function to apply elementwise (e.g., |
how |
Character; |
fill |
Numeric; value used to fill gaps when |
A data.frame with Date and the operated symbol columns.
## Not run: out <- ml_panel_op(mom12_panel, vol_panel, op = `*`) ## End(Not run)## Not run: out <- ml_panel_op(mom12_panel, vol_panel, op = `*`) ## End(Not run)
Folds a list of panels using ml_panel_op() across a set of named panels.
ml_panel_reduce(features, panels, op = `*`, how = "intersect", fill = NA_real_)ml_panel_reduce(features, panels, op = `*`, how = "intersect", fill = NA_real_)
features |
List of panels (each a wide data frame with |
panels |
Character vector of names in |
op |
Binary function to apply elementwise. |
how |
Column-set policy passed to |
fill |
Fill value for |
A data.frame panel (wide) with the reduced result.
## Not run: # product of three panels prod_panel <- ml_panel_reduce(X, c("mom12","vol","rsi14"), op = `*`) ## End(Not run)## Not run: # product of three panels prod_panel <- ml_panel_reduce(X, c("mom12","vol","rsi14"), op = `*`) ## End(Not run)
Computes the IC time series via ml_ic_series_on_scores() and plots the
rolling mean IC over a specified window. Returns the rolling statistics
invisibly for further inspection.
ml_plot_ic_roll(scores_dt, labels_dt, window = 26L, method = "spearman")ml_plot_ic_roll(scores_dt, labels_dt, window = 26L, method = "spearman")
scores_dt |
Scores (wide). |
labels_dt |
Labels (wide). |
window |
Integer window length (default 26). |
method |
Correlation method; |
(Invisibly) a data frame with Date, roll_mean, roll_sd, roll_ICIR.
## Not run: ris <- ml_plot_ic_roll(res_xgb$scores, Y, window = 8L) ## End(Not run)## Not run: ris <- ml_plot_ic_roll(res_xgb$scores, Y, window = 8L) ## End(Not run)
Constructs a minimal, leakage-safe set of tabular features commonly used
in cross-sectional ML: weekly momentum (12/26/52), RSI(14), distance from
20-week MA, and daily rolling volatility aligned to weekly dates
(tokens vol{N}d_walign, e.g., "vol20d_walign").
ml_prepare_features( prices_weekly, prices_daily = NULL, include = c("mom12", "mom26", "mom52", "vol20d_walign", "dist20", "rsi14"), interactions = NULL )ml_prepare_features( prices_weekly, prices_daily = NULL, include = c("mom12", "mom26", "mom52", "vol20d_walign", "dist20", "rsi14"), interactions = NULL )
prices_weekly |
Wide panel with |
prices_daily |
Optional wide panel (daily) if |
include |
Character vector of feature tokens to include. |
interactions |
Optional named list passed to |
All outputs are lagged by one period to avoid look-ahead in backtests.
A named list of panels (each a wide data.frame with Date).
## Not run: X <- ml_prepare_features(sample_prices_weekly, sample_prices_daily, include = c("mom12","vol20d_walign","rsi14")) ## End(Not run)## Not run: X <- ml_prepare_features(sample_prices_weekly, sample_prices_daily, include = c("mom12","vol20d_walign","rsi14")) ## End(Not run)
Given a wide panel with a Date column followed by symbol columns, returns
the same shape with each symbol column lagged by k periods. The Date
column is preserved; leading values introduced by the lag are NA_real_.
panel_lag(df, k = 1L)panel_lag(df, k = 1L)
df |
data.frame or data.table with columns |
k |
Integer lag (>= 1). |
A data.table with the same columns as df, lagged by k.
x <- data.frame(Date = as.Date("2020-01-01") + 0:2, A = 1:3, B = 11:13) panel_lag(x, 1L)x <- data.frame(Date = as.Date("2020-01-01") + 0:2, A = 1:3, B = 11:13) panel_lag(x, 1L)
Converts a wide price panel (Date + symbols) into arithmetic simple returns at the same cadence, dropping the first row per symbol.
panel_returns_simple(prices)panel_returns_simple(prices)
prices |
A data frame or data.table with columns |
A data frame with Date and one column per symbol containing simple returns
.
data(sample_prices_weekly) rets <- panel_returns_simple(sample_prices_weekly) head(rets)data(sample_prices_weekly) rets <- panel_returns_simple(sample_prices_weekly) head(rets)
Portfolio performance metrics
perf_metrics(ret_dt, freq = 52)perf_metrics(ret_dt, freq = 52)
ret_dt |
data.table/data.frame with columns |
freq |
Integer periods-per-year for annualization (e.g., 52 or 252). |
list with total_return, ann_return, ann_vol, sharpe, max_drawdown.
S3 plot method for visualizing backtest performance.
## S3 method for class 'backtest_result' plot(x, type = "performance", ...)## S3 method for class 'backtest_result' plot(x, type = "performance", ...)
x |
backtest_result object |
type |
Plot type: "performance", "drawdown", "weights", or "all" |
... |
Additional plotting parameters |
NULL (creates plot)
data("sample_prices_weekly") mom <- calc_momentum(sample_prices_weekly, lookback = 12) sel <- filter_top_n(mom, n = 10) W <- weight_equally(sel) res <- run_backtest(sample_prices_weekly, W) if (interactive()) plot(res, type = "performance")data("sample_prices_weekly") mom <- calc_momentum(sample_prices_weekly, lookback = 12) sel <- filter_top_n(mom, n = 10) W <- weight_equally(sel) res <- run_backtest(sample_prices_weekly, W) if (interactive()) plot(res, type = "performance")
Generic plotter for objects returned by run_param_grid(). Supported types:
"line": 1D metric vs one parameter.
"heatmap": 2D heatmap over two parameters.
"surface": 3D surface (persp) over two parameters.
"slices": 2D heatmaps faceted by a third parameter.
"surface_slices": 3D surfaces faceted by a third parameter.
"auto": chosen from the above based on the number of parameters.
## S3 method for class 'param_grid_result' plot( x, y = NULL, type = c("auto", "line", "heatmap", "surface", "slices", "surface_slices"), metric = NULL, params = NULL, fixed = list(), agg = c("mean", "median", "max"), na.rm = TRUE, palette = NULL, zlim = NULL, clip = c(0.02, 0.98), main = NULL, sub = NULL, xlab = NULL, ylab = NULL, ... )## S3 method for class 'param_grid_result' plot( x, y = NULL, type = c("auto", "line", "heatmap", "surface", "slices", "surface_slices"), metric = NULL, params = NULL, fixed = list(), agg = c("mean", "median", "max"), na.rm = TRUE, palette = NULL, zlim = NULL, clip = c(0.02, 0.98), main = NULL, sub = NULL, xlab = NULL, ylab = NULL, ... )
x |
A |
y |
Ignored. |
type |
One of "auto","line","heatmap","surface","slices","surface_slices". |
metric |
Column name to plot (defaults to "score" if present). |
params |
Character vector of parameter columns to use for axes/facets.
If |
fixed |
Optional named list of parameter values to condition on. Rows not matching are dropped before plotting. |
agg |
Aggregation when multiple rows map to a cell: "mean","median", or "max". |
na.rm |
Logical; drop NA metric rows before plotting. |
palette |
Optional color vector used for heatmaps/surfaces.
Defaults to |
zlim |
Optional two-element numeric range for color scaling. |
clip |
Two quantiles used to winsorize z-limits for stable coloring. |
main, sub, xlab, ylab
|
Base plotting annotations. |
... |
Additional options depending on |
An invisible list describing the plot (kind/params/zlim/etc.).
run_param_grid(), print.param_grid_result()
data(sample_prices_weekly) b <- function(prices, params, ...) { weight_equally(filter_top_n(calc_momentum(prices, params$lookback), 10)) } opt <- run_param_grid( prices = sample_prices_weekly, grid = list(lookback = c(8, 12, 26)), builder = b ) plot(opt, type = "line", params = "lookback")data(sample_prices_weekly) b <- function(prices, params, ...) { weight_equally(filter_top_n(calc_momentum(prices, params$lookback), 10)) } opt <- run_param_grid( prices = sample_prices_weekly, grid = list(lookback = c(8, 12, 26)), builder = b ) plot(opt, type = "line", params = "lookback")
S3 method for visualizing performance metrics. Supports multiple plot types including summary dashboard, return distributions, risk evolution, and rolling statistics.
## S3 method for class 'performance_analysis' plot(x, type = "summary", ...)## S3 method for class 'performance_analysis' plot(x, type = "summary", ...)
x |
performance_analysis object |
type |
Plot type: "summary", "returns", "risk", "drawdown" |
... |
Additional plotting parameters |
NULL (creates plot)
data("sample_prices_weekly") data("sample_prices_daily") syms_all <- intersect(names(sample_prices_weekly)[-1], names(sample_prices_daily)[-1]) syms <- syms_all[seq_len(min(3L, length(syms_all)))] P <- sample_prices_weekly[, c("Date", syms), with = FALSE] D <- sample_prices_daily[, c("Date", syms), with = FALSE] mom <- calc_momentum(P, lookback = 12) sel <- filter_top_n(mom, n = 3) W <- weight_equally(sel) res <- run_backtest(P, W) perf <- analyze_performance(res, D, benchmark_symbol = syms[1]) if (interactive()) { plot(perf, type = "summary") }data("sample_prices_weekly") data("sample_prices_daily") syms_all <- intersect(names(sample_prices_weekly)[-1], names(sample_prices_daily)[-1]) syms <- syms_all[seq_len(min(3L, length(syms_all)))] P <- sample_prices_weekly[, c("Date", syms), with = FALSE] D <- sample_prices_daily[, c("Date", syms), with = FALSE] mom <- calc_momentum(P, lookback = 12) sel <- filter_top_n(mom, n = 3) W <- weight_equally(sel) res <- run_backtest(P, W) perf <- analyze_performance(res, D, benchmark_symbol = syms[1]) if (interactive()) { plot(perf, type = "summary") }
Visual diagnostics for a wf_optimization_result returned by
run_walk_forward(). Supported types:
"parameters": best parameter values chosen per window.
"is_oos": in-sample vs out-of-sample scores by window.
"equity": stitched out-of-sample equity curve.
"drawdown": drawdown of the stitched OOS curve.
"windows": per-window bar/line chart of an OOS metric (see metric).
"stability": summary of parameter stability.
"distributions": distributions of IS/OOS scores across windows.
## S3 method for class 'wf_optimization_result' plot( x, y = NULL, type = c("parameters", "is_oos", "equity", "drawdown", "windows", "stability", "distributions"), param = NULL, metric = NULL, main = NULL, sub = NULL, xlab = NULL, ylab = NULL, ... )## S3 method for class 'wf_optimization_result' plot( x, y = NULL, type = c("parameters", "is_oos", "equity", "drawdown", "windows", "stability", "distributions"), param = NULL, metric = NULL, main = NULL, sub = NULL, xlab = NULL, ylab = NULL, ... )
x |
A |
y |
Ignored. |
type |
One of "parameters","is_oos","equity","drawdown", "windows","stability","distributions". |
param |
Character vector of parameter names to include for
"parameters"/"stability"/"distributions". If |
metric |
Character; column to plot for "is_oos" or "windows" (e.g., "OOS_Return" or "OOS_Score"). Ignored for other types. |
main, sub, xlab, ylab
|
Base plotting annotations. |
... |
Additional plot options (type-specific). |
Invisibly, a small list describing the plot.
run_walk_forward(), wf_report(), print.wf_optimization_result()
data(sample_prices_weekly) b <- function(prices, params, ...) { weight_equally(filter_top_n(calc_momentum(prices, params$lookback), params$top_n)) } wf <- run_walk_forward( prices = sample_prices_weekly, grid = list(lookback = c(8, 12, 26), top_n = c(5, 10)), builder = b, is_periods = 52, oos_periods = 26, step = 26 ) plot(wf, type = "parameters") plot(wf, type = "is_oos", metric = "OOS_Score")data(sample_prices_weekly) b <- function(prices, params, ...) { weight_equally(filter_top_n(calc_momentum(prices, params$lookback), params$top_n)) } wf <- run_walk_forward( prices = sample_prices_weekly, grid = list(lookback = c(8, 12, 26), top_n = c(5, 10)), builder = b, is_periods = 52, oos_periods = 26, step = 26 ) plot(wf, type = "parameters") plot(wf, type = "is_oos", metric = "OOS_Score")
Computes the portfolio simple return series by applying (lagged) portfolio weights to next-period asset returns, optionally net of proportional costs.
portfolio_returns(weights, prices, cost_bps = 0)portfolio_returns(weights, prices, cost_bps = 0)
weights |
A data.frame/data.table of portfolio weights on rebalance dates:
first column |
prices |
A data.frame/data.table of adjusted prices at the same cadence:
first column |
cost_bps |
One-way proportional cost per side in basis points (e.g., |
CASH support: if weights contains a column named "CASH" (case-insensitive)
but prices has no matching column, a synthetic flat price series is added
internally (price = 1 return = 0). In that case the function does not
re-normalise the non-CASH weights; the row is treated as a complete budget
(symbols + CASH = 1).
The function carries forward the latest available weights to each return row via the usual one-period decision lag. Transaction cost handling is conservative: if a turnover helper is not available, costs are skipped.
A data.table with columns Date and ret (portfolio simple return).
PortfolioTesteR::panel_returns_simple
data(sample_prices_weekly) mom12 <- PortfolioTesteR::calc_momentum(sample_prices_weekly, 12) sel10 <- PortfolioTesteR::filter_top_n(mom12, 10) w_eq <- PortfolioTesteR::weight_equally(sel10) pr <- portfolio_returns(w_eq, sample_prices_weekly, cost_bps = 0) head(pr)data(sample_prices_weekly) mom12 <- PortfolioTesteR::calc_momentum(sample_prices_weekly, 12) sel10 <- PortfolioTesteR::filter_top_n(mom12, 10) w_eq <- PortfolioTesteR::weight_equally(sel10) pr <- portfolio_returns(w_eq, sample_prices_weekly, cost_bps = 0) head(pr)
S3 print method for backtest results. Shows key performance metrics.
## S3 method for class 'backtest_result' print(x, ...)## S3 method for class 'backtest_result' print(x, ...)
x |
backtest_result object |
... |
Additional arguments (unused) |
Invisible copy of x
data("sample_prices_weekly") mom <- calc_momentum(sample_prices_weekly, lookback = 12) sel <- filter_top_n(mom, n = 10) W <- weight_equally(sel) res <- run_backtest(sample_prices_weekly, W) print(res)data("sample_prices_weekly") mom <- calc_momentum(sample_prices_weekly, lookback = 12) sel <- filter_top_n(mom, n = 10) W <- weight_equally(sel) res <- run_backtest(sample_prices_weekly, W) print(res)
Print a param_grid_result
## S3 method for class 'param_grid_result' print(x, ...)## S3 method for class 'param_grid_result' print(x, ...)
x |
A |
... |
Additional arguments passed to methods (ignored). |
Invisibly returns x.
S3 method for printing performance analysis with key metrics including risk-adjusted returns, drawdown statistics, and benchmark comparison.
## S3 method for class 'performance_analysis' print(x, ...)## S3 method for class 'performance_analysis' print(x, ...)
x |
performance_analysis object |
... |
Additional arguments (unused) |
Invisible copy of x
data("sample_prices_weekly") data("sample_prices_daily") syms_all <- intersect(names(sample_prices_weekly)[-1], names(sample_prices_daily)[-1]) syms <- syms_all[seq_len(min(3L, length(syms_all)))] P <- sample_prices_weekly[, c("Date", syms), with = FALSE] D <- sample_prices_daily[, c("Date", syms), with = FALSE] mom <- calc_momentum(P, lookback = 12) sel <- filter_top_n(mom, n = 3) W <- weight_equally(sel) res <- run_backtest(P, W) perf <- analyze_performance(res, D, benchmark_symbol = syms[1]) print(perf) # or just: perfdata("sample_prices_weekly") data("sample_prices_daily") syms_all <- intersect(names(sample_prices_weekly)[-1], names(sample_prices_daily)[-1]) syms <- syms_all[seq_len(min(3L, length(syms_all)))] P <- sample_prices_weekly[, c("Date", syms), with = FALSE] D <- sample_prices_daily[, c("Date", syms), with = FALSE] mom <- calc_momentum(P, lookback = 12) sel <- filter_top_n(mom, n = 3) W <- weight_equally(sel) res <- run_backtest(P, W) perf <- analyze_performance(res, D, benchmark_symbol = syms[1]) print(perf) # or just: perf
Print a wf_optimization_result
## S3 method for class 'wf_optimization_result' print(x, ...)## S3 method for class 'wf_optimization_result' print(x, ...)
x |
A |
... |
Additional arguments passed to methods (ignored). |
Invisibly returns x.
ml_backtest_multi() runsBuilds a compact set of outputs—coverage, IC series, OOS-only rolling IC,
performance tables (Full/Pre/Post), turnover, and a cost sweep—given two
lists of backtests (pooled and per-group) produced by ml_backtest_multi().
pt_collect_results( bt_pooled_list, bt_neutral_list, weekly_prices, horizons, split_date, cost_bps = c(5, 10, 15, 20, 25, 50), freq = 52, prices = NULL, ic_roll_window = 26L, mask_scores_to_decision_dates = TRUE, cost_model = function(turnover, bps) (bps/10000) * turnover )pt_collect_results( bt_pooled_list, bt_neutral_list, weekly_prices, horizons, split_date, cost_bps = c(5, 10, 15, 20, 25, 50), freq = 52, prices = NULL, ic_roll_window = 26L, mask_scores_to_decision_dates = TRUE, cost_model = function(turnover, bps) (bps/10000) * turnover )
bt_pooled_list |
Named list of backtests (keys like |
bt_neutral_list |
Named list of backtests (same keys) produced with
|
weekly_prices |
Deprecated alias for |
horizons |
Integer vector of horizons (in weeks) expected in the lists. |
split_date |
|
cost_bps |
Numeric vector of per-rebalance cost levels (in basis points)
for the turnover-based cost sweep. Default |
freq |
Integer frequency used by |
prices |
Optional price table (preferred). If |
ic_roll_window |
Integer window length (weeks) for rolling IC on OOS decision dates.
Default |
mask_scores_to_decision_dates |
Logical; if |
cost_model |
Function |
Both input lists must have identical horizon keys (paste0("H", h, "w")).
Coverage and IC series are computed from stored scores; rolling IC is built
on OOS decision dates only; performance is summarised for the full sample
and Pre/Post relative to split_date; turnover is derived from realised
sector-neutral weights; and a turnover-based cost sweep is evaluated on the
sector-neutral run across cost_bps.
A named list with one element per horizon, each containing:
bt_pooled, bt_neutral — the input backtests;
coverage — coverage by date for pooled/neutral;
ic_series — raw IC series for pooled/neutral;
icroll_oos_26w — rolling IC (OOS-only) for pooled/neutral;
masked_scores — OOS-masked score tables for pooled/neutral;
perf_tables — performance tables (Full/Pre/Post);
turnover_neutral — turnover series for the sector-neutral run;
cost_sweep_neutral — performance under gross/net across cost_bps.
ml_backtest_multi(), scores_oos_only(), perf_metrics()
Other Chapter3-helpers:
scores_oos_only()
if (requireNamespace("PortfolioTesteR", quietly = TRUE)) { library(PortfolioTesteR) data(sample_prices_weekly, package = "PortfolioTesteR") # Simple feature mom12 <- PortfolioTesteR::calc_momentum(sample_prices_weekly, 12) feats <- list(mom12 = mom12) fit_first <- function(X, y, ...) list() predict_first <- function(model, Xnew, ...) as.numeric(Xnew[[1]]) sch <- list(is = 52L, oos = 26L, step = 26L) syms <- setdiff(names(sample_prices_weekly), "Date") gmap <- data.frame(Symbol = syms, Group = rep(c("G1","G2"), length.out = length(syms))) bt_pooled <- ml_backtest_multi(feats, sample_prices_weekly, c(4L), fit_first, predict_first, sch, selection = list(top_k = 5L), weighting = list(method = "softmax", temperature = 12), caps = list(max_per_symbol = 0.10), group_mode = "pooled") bt_neutral <- ml_backtest_multi(feats, sample_prices_weekly, c(4L), fit_first, predict_first, sch, selection = list(top_k = 5L), weighting = list(method = "softmax", temperature = 12), caps = list(max_per_symbol = 0.10), group_mode = "per_group", group_map = gmap) out <- pt_collect_results( bt_pooled_list = bt_pooled, bt_neutral_list = bt_neutral, prices = sample_prices_weekly, horizons = c(4L), split_date = as.Date("2019-01-01"), cost_bps = c(5, 15), freq = 52, ic_roll_window = 13L ) names(out) str(out[["H4w"]]$perf_tables) }if (requireNamespace("PortfolioTesteR", quietly = TRUE)) { library(PortfolioTesteR) data(sample_prices_weekly, package = "PortfolioTesteR") # Simple feature mom12 <- PortfolioTesteR::calc_momentum(sample_prices_weekly, 12) feats <- list(mom12 = mom12) fit_first <- function(X, y, ...) list() predict_first <- function(model, Xnew, ...) as.numeric(Xnew[[1]]) sch <- list(is = 52L, oos = 26L, step = 26L) syms <- setdiff(names(sample_prices_weekly), "Date") gmap <- data.frame(Symbol = syms, Group = rep(c("G1","G2"), length.out = length(syms))) bt_pooled <- ml_backtest_multi(feats, sample_prices_weekly, c(4L), fit_first, predict_first, sch, selection = list(top_k = 5L), weighting = list(method = "softmax", temperature = 12), caps = list(max_per_symbol = 0.10), group_mode = "pooled") bt_neutral <- ml_backtest_multi(feats, sample_prices_weekly, c(4L), fit_first, predict_first, sch, selection = list(top_k = 5L), weighting = list(method = "softmax", temperature = 12), caps = list(max_per_symbol = 0.10), group_mode = "per_group", group_map = gmap) out <- pt_collect_results( bt_pooled_list = bt_pooled, bt_neutral_list = bt_neutral, prices = sample_prices_weekly, horizons = c(4L), split_date = as.Date("2019-01-01"), cost_bps = c(5, 15), freq = 52, ic_roll_window = 13L ) names(out) str(out[["H4w"]]$perf_tables) }
Ranks stocks within their sector for sector-neutral strategies. Enables selecting best stocks from each sector regardless of sector performance. Optimized using matrix operations within groups.
rank_within_sector( indicator_df, sector_mapping, method = c("percentile", "rank", "z-score"), min_sector_size = 3 )rank_within_sector( indicator_df, sector_mapping, method = c("percentile", "rank", "z-score"), min_sector_size = 3 )
indicator_df |
Data frame with Date column and indicator values |
sector_mapping |
Data frame with |
method |
"percentile" (0-100), "rank" (1-N), or "z-score" |
min_sector_size |
Minimum stocks per sector (default: 3) |
Data frame with within-sector ranks/scores
data("sample_prices_weekly") data("sample_sp500_sectors") momentum <- calc_momentum(sample_prices_weekly, 12) sector_ranks <- rank_within_sector(momentum, sample_sp500_sectors)data("sample_prices_weekly") data("sample_sp500_sectors") momentum <- calc_momentum(sample_prices_weekly, 12) sector_ranks <- rank_within_sector(momentum, sample_sp500_sectors)
Rebalance calendar (rows with non-zero allocation)
rebalance_calendar(weights)rebalance_calendar(weights)
weights |
Wide weight panel (Date + symbols). |
data.table with Date, row indices where a rebalance occurred.
Rolling fit/predict for tabular features (pooled / per-symbol / per-group)
roll_fit_predict( features_list, label, fit_fn, predict_fn, is_periods = 156L, oos_periods = 4L, step = 4L, group = c("pooled", "per_symbol", "per_group"), group_map = NULL, na_action = c("omit", "zero") )roll_fit_predict( features_list, label, fit_fn, predict_fn, is_periods = 156L, oos_periods = 4L, step = 4L, group = c("pooled", "per_symbol", "per_group"), group_map = NULL, na_action = c("omit", "zero") )
features_list |
list of wide panels (each: |
label |
wide panel of future returns (same symbol set as features). |
fit_fn |
function |
predict_fn |
function |
is_periods, oos_periods, step
|
ints; in-sample length, out-of-sample length, and step size for the rolling window. |
group |
one of |
group_map |
optional |
na_action |
|
wide panel of scores: Date + symbols.
data(sample_prices_weekly); data(sample_prices_daily) mom <- panel_lag(calc_momentum(sample_prices_weekly, 12), 1) vol <- panel_lag(align_to_timeframe( calc_rolling_volatility(sample_prices_daily, 20), sample_prices_weekly$Date, "forward_fill"), 1) Y <- make_labels(sample_prices_weekly, horizon = 4, type = "log") fit_lm <- function(X,y){ Xc <- cbind(1,X); list(coef=stats::lm.fit(Xc,y)$coefficients) } pred_lm <- function(m,X){ as.numeric(cbind(1,X) %*% m$coef) } S <- roll_fit_predict(list(mom=mom, vol=vol), Y, fit_lm, pred_lm, 52, 4, 4) head(S)data(sample_prices_weekly); data(sample_prices_daily) mom <- panel_lag(calc_momentum(sample_prices_weekly, 12), 1) vol <- panel_lag(align_to_timeframe( calc_rolling_volatility(sample_prices_daily, 20), sample_prices_weekly$Date, "forward_fill"), 1) Y <- make_labels(sample_prices_weekly, horizon = 4, type = "log") fit_lm <- function(X,y){ Xc <- cbind(1,X); list(coef=stats::lm.fit(Xc,y)$coefficients) } pred_lm <- function(m,X){ as.numeric(cbind(1,X) %*% m$coef) } S <- roll_fit_predict(list(mom=mom, vol=vol), Y, fit_lm, pred_lm, 52, 4, 4) head(S)
Rolling fit/predict for sequence models (flattened steps-by-p features)
roll_fit_predict_seq( features_list, labels, steps = 26L, horizon = 4L, fit_fn, predict_fn, is_periods = 156L, oos_periods = 4L, step = 4L, group = c("pooled", "per_symbol", "per_group"), group_map = NULL, normalize = c("none", "zscore", "minmax"), min_train_samples = 50L, na_action = c("omit", "zero") )roll_fit_predict_seq( features_list, labels, steps = 26L, horizon = 4L, fit_fn, predict_fn, is_periods = 156L, oos_periods = 4L, step = 4L, group = c("pooled", "per_symbol", "per_group"), group_map = NULL, normalize = c("none", "zscore", "minmax"), min_train_samples = 50L, na_action = c("omit", "zero") )
features_list |
list of panels to be stacked over |
labels |
future-return panel aligned to the features. |
steps |
int; lookback length (e.g., 26). |
horizon |
int; label horizon (e.g., 4). |
fit_fn |
function |
predict_fn |
function |
is_periods, oos_periods, step
|
ints; in-sample length, out-of-sample length, and step size for the rolling window. |
group |
one of |
group_map |
optional |
normalize |
|
min_train_samples |
Optional minimum IS samples required to fit; if not met, skip fit. |
na_action |
|
wide panel of scores.
data(sample_prices_weekly); data(sample_prices_daily) mom <- panel_lag(calc_momentum(sample_prices_weekly, 12), 1) vol <- panel_lag(align_to_timeframe( calc_rolling_volatility(sample_prices_daily, 20), sample_prices_weekly$Date, "forward_fill"), 1) Y <- make_labels(sample_prices_weekly, horizon = 4, type = "log") fit_lm <- function(X,y){ Xc <- cbind(1,X); list(coef=stats::lm.fit(Xc,y)$coefficients) } pred_lm <- function(m,X){ as.numeric(cbind(1,X) %*% m$coef) } S <- roll_fit_predict_seq(list(mom=mom, vol=vol), Y, steps = 26, horizon = 4, fit_fn = fit_lm, predict_fn = pred_lm, is_periods = 104, oos_periods = 4, step = 4) head(S)data(sample_prices_weekly); data(sample_prices_daily) mom <- panel_lag(calc_momentum(sample_prices_weekly, 12), 1) vol <- panel_lag(align_to_timeframe( calc_rolling_volatility(sample_prices_daily, 20), sample_prices_weekly$Date, "forward_fill"), 1) Y <- make_labels(sample_prices_weekly, horizon = 4, type = "log") fit_lm <- function(X,y){ Xc <- cbind(1,X); list(coef=stats::lm.fit(Xc,y)$coefficients) } pred_lm <- function(m,X){ as.numeric(cbind(1,X) %*% m$coef) } S <- roll_fit_predict_seq(list(mom=mom, vol=vol), Y, steps = 26, horizon = 4, fit_fn = fit_lm, predict_fn = pred_lm, is_periods = 104, oos_periods = 4, step = 4) head(S)
Compute rolling information coefficient (IC) statistics from a per-date IC series.
roll_ic_stats(ic_dt, window = 26L)roll_ic_stats(ic_dt, window = 26L)
ic_dt |
Data frame/data.table produced by |
window |
Integer window length for the rolling statistics. |
For each rolling window, compute the mean IC, the standard deviation of IC,
and the information coefficient ratio (ICIR = mean / sd). Windows with fewer
than two finite IC values yield NA for ICIR.
A data.table with columns Date, IC_mean, IC_sd, and ICIR.
Main backtesting engine that simulates portfolio performance over time. Handles position tracking, transaction costs, and performance calculation.
run_backtest( prices, weights, initial_capital = 1e+05, name = "Strategy", verbose = FALSE, stop_loss = NULL, stop_monitoring_prices = NULL )run_backtest( prices, weights, initial_capital = 1e+05, name = "Strategy", verbose = FALSE, stop_loss = NULL, stop_monitoring_prices = NULL )
prices |
Price data (data.frame with Date column) |
weights |
Weight matrix from weighting functions |
initial_capital |
Starting capital (default: 100000) |
name |
Strategy name for reporting |
verbose |
Print progress messages (default: FALSE) |
stop_loss |
Optional stop loss percentage as decimal |
stop_monitoring_prices |
Optional daily prices for stop monitoring |
backtest_result object with performance metrics
data("sample_prices_weekly") momentum <- calc_momentum(sample_prices_weekly, lookback = 12) selected <- filter_top_n(momentum, n = 10) weights <- weight_equally(selected) result <- run_backtest(sample_prices_weekly, weights)data("sample_prices_weekly") momentum <- calc_momentum(sample_prices_weekly, lookback = 12) selected <- filter_top_n(momentum, n = 10) weights <- weight_equally(selected) result <- run_backtest(sample_prices_weekly, weights)
Executes an example script bundled in the package inst/examples/ folder.
run_example(example_name, echo = TRUE)run_example(example_name, echo = TRUE)
example_name |
Character scalar with the example filename (e.g. |
echo |
Logical; print code as it runs (default |
Invisibly returns NULL. Runs the example for its side effects.
# Example (requires a real file under inst/examples): # run_example("basic.R")# Example (requires a real file under inst/examples): # run_example("basic.R")
Run Parameter Grid Optimization (safe + ergonomic)
run_param_grid( prices, grid, builder, metric = NULL, name_prefix = "Strategy", verbose = FALSE, light_mode = TRUE, precompute_returns = TRUE, builder_args = list(), n_cores = 1, fixed = NULL )run_param_grid( prices, grid, builder, metric = NULL, name_prefix = "Strategy", verbose = FALSE, light_mode = TRUE, precompute_returns = TRUE, builder_args = list(), n_cores = 1, fixed = NULL )
prices |
Data frame with Date + symbol columns |
grid |
Data frame (each row = a combo) OR a named list of vectors |
builder |
Function(prices, params, ...) -> weights (Date + symbols) |
metric |
Scoring function(backtest) -> numeric. Defaults to metric_sharpe. |
name_prefix |
String prefix for backtest names |
verbose |
Logical |
light_mode |
Logical: speed-ups in backtest |
precompute_returns |
Logical: precompute log-returns once (light_mode only) |
builder_args |
List of extra args forwarded to builder (e.g., caches) |
n_cores |
Integer (kept for API compatibility; ignored here) |
fixed |
Optional named list of constant parameters merged into every combo.
If a name appears in both |
param_grid_result
Runs rolling IS/OOS optimization, reselects params each window, and backtests OOS performance (optionally with warmup tails).
run_walk_forward( prices, grid, builder, metric = NULL, is_periods = 52, oos_periods = 13, step = NULL, warmup_periods = 0, verbose = FALSE, light_mode = TRUE, precompute_all = TRUE, builder_args = list(), n_cores = 1 )run_walk_forward( prices, grid, builder, metric = NULL, is_periods = 52, oos_periods = 13, step = NULL, warmup_periods = 0, verbose = FALSE, light_mode = TRUE, precompute_all = TRUE, builder_args = list(), n_cores = 1 )
prices |
Data frame with Date column and symbol columns |
grid |
Data frame OR named list; each row/combination is a parameter set |
builder |
Function(prices, params, ...) -> weights data.frame (Date + assets) |
metric |
Function(backtest_result) -> scalar score (higher is better).
Defaults to |
is_periods |
Integer, number of in-sample periods |
oos_periods |
Integer, number of out-of-sample periods |
step |
Integer, step size for rolling windows (default = oos_periods) |
warmup_periods |
Integer, warmup periods appended before each OOS |
verbose |
Logical, print progress |
light_mode |
Logical, passed to run_param_grid (kept for compatibility) |
precompute_all |
Logical, precompute indicators once and slice per window |
builder_args |
List, extra args passed to builder (e.g., indicator_cache) |
n_cores |
Integer (kept for API compatibility; ignored here) |
An object of class wf_optimization_result.
Performs division with automatic handling of NA values, zeros, and infinity. Returns 0 for division by zero and NA cases.
safe_divide(numerator, denominator)safe_divide(numerator, denominator)
numerator |
Numeric vector |
denominator |
Numeric vector |
Numeric vector with safe division results
safe_divide(c(10, 0, NA, 5), c(2, 0, 5, NA)) # Returns c(5, 0, 0, 0)safe_divide(c(10, 0, NA, 5), c(2, 0, 5, NA)) # Returns c(5, 0, 0, 0)
Daily closing prices for 20 stocks from 2017-2019. Contains the same symbols as sample_prices_weekly but at daily frequency for more granular analysis and performance calculations.
data(sample_prices_daily)data(sample_prices_daily)
A data.table with 754 rows and 21 columns:
Date object, trading date
Apple Inc. adjusted closing price
Amazon.com Inc. adjusted closing price
Boeing Co. adjusted closing price
Bank of America Corp. adjusted closing price
Additional stock symbols with adjusted closing prices
Yahoo Finance historical data, adjusted for splits and dividends
data(sample_prices_daily) head(sample_prices_daily) # Get date range range(sample_prices_daily$Date)data(sample_prices_daily) head(sample_prices_daily) # Get date range range(sample_prices_daily$Date)
Weekly closing prices for 20 stocks from 2017-2019. Data includes major stocks from various sectors and is suitable for demonstrating backtesting and technical analysis functions.
data(sample_prices_weekly)data(sample_prices_weekly)
A data.table with 158 rows and 21 columns:
Date object, weekly closing date (typically Friday)
Apple Inc. adjusted closing price
Amazon.com Inc. adjusted closing price
Boeing Co. adjusted closing price
Bank of America Corp. adjusted closing price
Additional stock symbols with adjusted closing prices
Yahoo Finance historical data, adjusted for splits and dividends
data(sample_prices_weekly) head(sample_prices_weekly) # Calculate momentum momentum <- calc_momentum(sample_prices_weekly, lookback = 12)data(sample_prices_weekly) head(sample_prices_weekly) # Calculate momentum momentum <- calc_momentum(sample_prices_weekly, lookback = 12)
Sector classifications for the stock symbols in the sample datasets. Note: ETFs (SPY, QQQ, etc.) are not included as they represent indices or sectors themselves rather than individual companies.
data(sample_sp500_sectors)data(sample_sp500_sectors)
A data.table with 18 rows and 2 columns:
Character, stock ticker symbol
Character, GICS sector classification
S&P 500 constituent data
data(sample_sp500_sectors) head(sample_sp500_sectors) # Count stocks per sector table(sample_sp500_sectors$Sector)data(sample_sp500_sectors) head(sample_sp500_sectors) # Count stocks per sector table(sample_sp500_sectors$Sector)
Utility used in the chapter’s diagnostics: keep scores only on dates when a
portfolio decision was actually made (non-zero realised weights); set other
dates to NA. Inputs are wide by symbol with a Date column.
scores_oos_only(scores_dt, weights_wide)scores_oos_only(scores_dt, weights_wide)
scores_dt |
Wide table of model scores with columns |
weights_wide |
Wide table of realised portfolio weights with columns
|
A copy of scores_dt where rows not matching decision dates are set to NA
(except the Date column). If either input is empty, returns scores_dt[0].
Other Chapter3-helpers:
pt_collect_results()
# Toy example dates <- as.Date("2020-01-01") + 7*(0:5) scores <- data.frame( Date = dates, AAA = seq(0.1, 0.6, length.out = 6), BBB = rev(seq(0.1, 0.6, length.out = 6)) ) weights <- data.frame( Date = dates, AAA = c(0, 0.1, 0, 0.2, 0, 0.15), BBB = c(0, 0, 0, 0, 0, 0 ) ) scores_oos_only(scores, weights)# Toy example dates <- as.Date("2020-01-01") + 7*(0:5) scores <- data.frame( Date = dates, AAA = seq(0.1, 0.6, length.out = 6), BBB = rev(seq(0.1, 0.6, length.out = 6)) ) weights <- data.frame( Date = dates, AAA = c(0, 0.1, 0, 0.2, 0, 0.15), BBB = c(0, 0, 0, 0, 0, 0 ) ) scores_oos_only(scores, weights)
Select top-K scores per date
select_top_k_scores(scores, k, ties = "first")select_top_k_scores(scores, k, ties = "first")
scores |
score panel. |
k |
integer; how many to keep per date. |
ties |
Ties method passed to base::rank (e.g., 'first','average'). |
logical mask panel (Date + symbols) marking selected names.
For each date, choose the top k symbols within each group based on a
score panel. Returns a logical selection panel aligned to the input.
select_top_k_scores_by_group(scores, k, group_map, max_per_group = 3L)select_top_k_scores_by_group(scores, k, group_map, max_per_group = 3L)
scores |
Wide score panel ( |
k |
Positive integer: number of symbols to select per group. |
group_map |
Named character vector or 2-column data.frame
( |
max_per_group |
Integer cap per group (default |
Group membership comes from group_map (symbol -> group).
Selection is computed independently by group on each date.
Ties follow the ordering implied by order(..., method = "radix").
Logical selection panel (Date + symbols) where TRUE marks
selected symbols.
set.seed(42) scores <- data.frame( Date = as.Date("2020-01-01") + 0:1, A = runif(2), B = runif(2), C = runif(2), D = runif(2), E = runif(2), F = runif(2) ) gmap <- data.frame(Symbol = c("A","B","C","D","E","F"), Group = c("G1","G1","G2","G2","G3","G3")) sel <- select_top_k_scores_by_group(scores, k = 4, group_map = gmap, max_per_group = 2) head(sel)set.seed(42) scores <- data.frame( Date = as.Date("2020-01-01") + 0:1, A = runif(2), B = runif(2), C = runif(2), D = runif(2), E = runif(2), F = runif(2) ) gmap <- data.frame(Symbol = c("A","B","C","D","E","F"), Group = c("G1","G1","G2","G2","G3","G3")) sel <- select_top_k_scores_by_group(scores, k = 4, group_map = gmap, max_per_group = 2) head(sel)
Loads stock price data from SQLite database with automatic frequency conversion.
sql_adapter( db_path, symbols, start_date = NULL, end_date = NULL, auto_update = TRUE, frequency = "daily" )sql_adapter( db_path, symbols, start_date = NULL, end_date = NULL, auto_update = TRUE, frequency = "daily" )
db_path |
Path to SQLite database file |
symbols |
Character vector of stock symbols to load |
start_date |
Start date (YYYY-MM-DD) or NULL |
end_date |
End date (YYYY-MM-DD) or NULL |
auto_update |
Auto-update database before loading (default: TRUE) |
frequency |
"daily", "weekly", or "monthly" (default: "daily") |
data.table with Date column and one column per symbol
prices <- sql_adapter( db_path = "sp500.db", symbols = c("AAPL", "MSFT"), start_date = "2020-01-01", end_date = "2020-12-31", frequency = "weekly" ) head(prices)prices <- sql_adapter( db_path = "sp500.db", symbols = c("AAPL", "MSFT"), start_date = "2020-01-01", end_date = "2020-12-31", frequency = "weekly" ) head(prices)
Loads adjusted stock prices (for splits/dividends) from SQLite.
sql_adapter_adjusted( db_path, symbols, start_date = NULL, end_date = NULL, auto_update = FALSE, frequency = "daily", use_adjusted = TRUE )sql_adapter_adjusted( db_path, symbols, start_date = NULL, end_date = NULL, auto_update = FALSE, frequency = "daily", use_adjusted = TRUE )
db_path |
Path to SQLite database file |
symbols |
Character vector of stock symbols to load |
start_date |
Start date (YYYY-MM-DD) or NULL |
end_date |
End date (YYYY-MM-DD) or NULL |
auto_update |
Auto-update database (default: FALSE) |
frequency |
"daily", "weekly", or "monthly" (default: "daily") |
use_adjusted |
Use adjusted prices if available (default: TRUE) |
data.table with Date column and adjusted prices per symbol
prices <- sql_adapter_adjusted( db_path = "sp500.db", symbols = c("AAPL", "MSFT"), start_date = "2020-01-01", end_date = "2020-12-31", frequency = "monthly" ) head(prices)prices <- sql_adapter_adjusted( db_path = "sp500.db", symbols = c("AAPL", "MSFT"), start_date = "2020-01-01", end_date = "2020-12-31", frequency = "monthly" ) head(prices)
Summary method for backtest results
## S3 method for class 'backtest_result' summary(object, ...)## S3 method for class 'backtest_result' summary(object, ...)
object |
A backtest_result object |
... |
Additional arguments (unused) |
Invisible copy of the object
Dynamically switches between two weighting schemes based on a signal. Enables tactical allocation changes.
switch_weights(weights_a, weights_b, use_b_condition, partial_blend = 1)switch_weights(weights_a, weights_b, use_b_condition, partial_blend = 1)
weights_a |
Primary weight matrix |
weights_b |
Alternative weight matrix |
use_b_condition |
Logical vector (TRUE = use weights_b) |
partial_blend |
Blend factor 0-1 (default: 1 = full switch) |
Combined weight matrix
data("sample_prices_weekly") momentum <- calc_momentum(sample_prices_weekly, lookback = 12) selected <- filter_top_n(momentum, n = 10) weights_equal <- weight_equally(selected) weights_signal <- weight_by_signal(selected, momentum) # Create switching signal (example: use SPY momentum as regime indicator) spy_momentum <- momentum$SPY switch_signal <- as.numeric(spy_momentum > median(spy_momentum, na.rm = TRUE)) switch_signal[is.na(switch_signal)] <- 0 # Switch between strategies final_weights <- switch_weights(weights_equal, weights_signal, switch_signal)data("sample_prices_weekly") momentum <- calc_momentum(sample_prices_weekly, lookback = 12) selected <- filter_top_n(momentum, n = 10) weights_equal <- weight_equally(selected) weights_signal <- weight_by_signal(selected, momentum) # Create switching signal (example: use SPY momentum as regime indicator) spy_momentum <- momentum$SPY switch_signal <- as.numeric(spy_momentum > median(spy_momentum, na.rm = TRUE)) switch_signal[is.na(switch_signal)] <- 0 # Switch between strategies final_weights <- switch_weights(weights_equal, weights_signal, switch_signal)
Per-date score transform (z-score or rank)
transform_scores( scores, method = c("zscore", "rank"), per_date = TRUE, robust = FALSE )transform_scores( scores, method = c("zscore", "rank"), per_date = TRUE, robust = FALSE )
scores |
wide panel |
method |
|
per_date |
logical; currently must be TRUE. |
robust |
logical; robust z-score via median/MAD. |
panel of transformed scores.
Quick grid tuning for tabular pipeline
tune_ml_backtest( features_list, labels, prices, fit_fn, predict_fn, schedule = list(is = 104L, oos = 4L, step = 4L), grid = list(top_k = c(10L, 15L), temperature = c(8, 12), method = c("softmax", "rank"), transform = c("zscore")), group = "pooled", selection_defaults = list(top_k = 15L, max_per_group = NULL), weighting_defaults = list(method = "softmax", temperature = 12, floor = 0), caps = list(max_per_symbol = 0.08), group_map = NULL, cost_bps = 0, freq = 52 )tune_ml_backtest( features_list, labels, prices, fit_fn, predict_fn, schedule = list(is = 104L, oos = 4L, step = 4L), grid = list(top_k = c(10L, 15L), temperature = c(8, 12), method = c("softmax", "rank"), transform = c("zscore")), group = "pooled", selection_defaults = list(top_k = 15L, max_per_group = NULL), weighting_defaults = list(method = "softmax", temperature = 12, floor = 0), caps = list(max_per_symbol = 0.08), group_map = NULL, cost_bps = 0, freq = 52 )
features_list |
List of feature panels. |
labels |
Label panel. |
prices |
Price panel used for backtests (Date + symbols). |
fit_fn, predict_fn
|
Model fit and predict functions. |
schedule |
List with elements is, oos, step. |
grid |
list of vectors: |
group |
Grouping mode for roll_fit_predict ('pooled'/'per_symbol'/'per_group'). |
selection_defaults |
Default selection settings (e.g., top_k). |
weighting_defaults |
Default weighting settings (e.g., method, temperature). |
caps |
Exposure caps (e.g., max_per_symbol/max_per_group). |
group_map |
Optional Symbol->Group mapping. |
cost_bps |
optional one-way cost in basis points for net performance. |
freq |
re-annualization frequency (e.g., 52). |
data.table with metrics per grid row.
Turnover by date
turnover_by_date(weights)turnover_by_date(weights)
weights |
weight panel. |
data.table with Date, turnover (0.5 * L1 change).
Update VIX data in database
update_vix_in_db(db_path, from_date = NULL)update_vix_in_db(db_path, from_date = NULL)
db_path |
Path to SQLite database |
from_date |
Start date for update (NULL = auto-detect) |
Number of rows updated (invisible)
Checks that data meets library requirements: proper Date column, at least one symbol, correct data types. Prints diagnostic info.
validate_data_format(data)validate_data_format(data)
data |
Data frame to validate |
TRUE if valid, stops with error if not
data("sample_prices_weekly") # Check if data is properly formatted validate_data_format(sample_prices_weekly)data("sample_prices_weekly") # Check if data is properly formatted validate_data_format(sample_prices_weekly)
Normalizes and checks a symbol → group mapping for a given set of symbols.
Accepts either a data.frame/data.table with columns Symbol and Group,
or a named character vector c(symbol = "group", ...).
Errors if any requested symbol is missing or mapped more than once.
validate_group_map(symbols, group_map)validate_group_map(symbols, group_map)
symbols |
Character vector of symbols to validate/keep. |
group_map |
Data frame/data.table with columns |
A two-column data.frame with columns Symbol and Group
(one row per symbol), sorted by Symbol.
validate_group_map( c("A","B"), data.frame(Symbol = c("A","B"), Group = c("G1","G1")) )validate_group_map( c("A","B"), data.frame(Symbol = c("A","B"), Group = c("G1","G1")) )
Quick leakage guard: date alignment & NA expectations
validate_no_leakage(features, labels, verbose = TRUE)validate_no_leakage(features, labels, verbose = TRUE)
features |
Wide feature panel with a |
labels |
Wide label panel (same |
verbose |
If TRUE, prints diagnostic messages. |
TRUE/FALSE (invisible), with messages if verbose = TRUE.
Scales each row of a wide weight table (Date + symbols) so the estimated
annualised portfolio volatility matches a target. Volatility is estimated
from a rolling covariance of simple asset returns computed from prices.
vol_target( weights, prices, lookback = 26L, target_annual = 0.12, periods_per_year = 52L, cap = TRUE )vol_target( weights, prices, lookback = 26L, target_annual = 0.12, periods_per_year = 52L, cap = TRUE )
weights |
data.frame/data.table with columns: |
prices |
data.frame/data.table of adjusted prices at the same cadence as |
lookback |
Integer window length (in periods) for the rolling covariance. Default 26. |
target_annual |
Annualised volatility target (e.g., 0.12 for 12%). Must be > 0. |
periods_per_year |
Number of periods per year used for annualisation (e.g., 52 for weekly). |
cap |
If TRUE (default), scale down only: exposure is reduced when the
estimated vol exceeds the target, and untouched otherwise. In this down-only mode,
the function adds/overwrites a |
Weights decided at t-1 apply to returns over t.
The covariance at row i is computed from the last lookback rows of simple
returns up to i (inclusive), estimated on the intersection of symbols present
in both weights and prices. The row scaler is
s_i = min(1, target_vol / est_vol_i) when cap = TRUE, and
s_i = target_vol / est_vol_i when cap = FALSE, with safeguards for zero or
non-finite variances.
A data.table with the same Date and symbol columns as weights
(plus CASH when cap = TRUE).
data(sample_prices_weekly) mom12 <- PortfolioTesteR::calc_momentum(sample_prices_weekly, 12) sel10 <- PortfolioTesteR::filter_top_n(mom12, 10) w_eq <- PortfolioTesteR::weight_equally(sel10) w_vt <- vol_target(w_eq, sample_prices_weekly, lookback = 26, target_annual = 0.12, periods_per_year = 52, cap = TRUE) head(w_vt)data(sample_prices_weekly) mom12 <- PortfolioTesteR::calc_momentum(sample_prices_weekly, 12) sel10 <- PortfolioTesteR::filter_top_n(mom12, 10) w_eq <- PortfolioTesteR::weight_equally(sel10) w_vt <- vol_target(w_eq, sample_prices_weekly, lookback = 26, target_annual = 0.12, periods_per_year = 52, cap = TRUE) head(w_vt)
Calculates portfolio weights using Hierarchical Risk Parity (HRP) methodology. HRP combines hierarchical clustering with risk-based allocation to create diversified portfolios that don't rely on unstable correlation matrix inversions.
weight_by_hrp( selected_df, prices_df, lookback_periods = 252, cluster_method = "ward.D2", distance_method = "euclidean", min_periods = 60, use_correlation = FALSE )weight_by_hrp( selected_df, prices_df, lookback_periods = 252, cluster_method = "ward.D2", distance_method = "euclidean", min_periods = 60, use_correlation = FALSE )
selected_df |
Binary selection matrix (data.frame with Date column) |
prices_df |
Price data for covariance calculation (typically daily) Returns are calculated internally from prices |
lookback_periods |
Number of periods for covariance estimation (default: 252) |
cluster_method |
Clustering linkage method (default: "ward.D2") |
distance_method |
Distance measure for clustering (default: "euclidean") |
min_periods |
Minimum periods required for calculation (default: 60) |
use_correlation |
If TRUE, cluster on correlation instead of covariance |
The HRP algorithm:
Calculate returns from input prices
Compute covariance matrix from returns
Cluster assets based on distance matrix
Apply recursive bisection with inverse variance weighting
Results in naturally diversified portfolio without matrix inversion
The function accepts price data and calculates returns internally, matching the pattern of other library functions like calc_momentum().
Weight matrix with same dates as selected_df
data("sample_prices_daily") data("sample_prices_weekly") # Create a selection first momentum <- calc_momentum(sample_prices_weekly, lookback = 12) selected <- filter_top_n(momentum, n = 10) # Using daily prices for risk calculation weights <- weight_by_hrp(selected, sample_prices_daily, lookback_periods = 252) # Using correlation-based clustering weights <- weight_by_hrp(selected, sample_prices_daily, use_correlation = TRUE)data("sample_prices_daily") data("sample_prices_weekly") # Create a selection first momentum <- calc_momentum(sample_prices_weekly, lookback = 12) selected <- filter_top_n(momentum, n = 10) # Using daily prices for risk calculation weights <- weight_by_hrp(selected, sample_prices_daily, lookback_periods = 252) # Using correlation-based clustering weights <- weight_by_hrp(selected, sample_prices_daily, use_correlation = TRUE)
Weights securities based on their rank rather than raw signal values. Useful when signal magnitudes are unreliable but ordering is meaningful.
weight_by_rank( selected_df, signal_df, method = c("linear", "exponential"), ascending = FALSE )weight_by_rank( selected_df, signal_df, method = c("linear", "exponential"), ascending = FALSE )
selected_df |
Binary selection matrix |
signal_df |
Signal values for ranking |
method |
Weighting method: "linear" or "exponential" |
ascending |
Sort order for ranking (default: FALSE) |
Data.table with rank-based weights
data("sample_prices_weekly") momentum <- calc_momentum(sample_prices_weekly, lookback = 12) selected <- filter_top_n(momentum, 10) # Linear rank weighting (best gets most) weights <- weight_by_rank(selected, momentum, method = "linear") # Exponential (heavy on top stocks) weights_exp <- weight_by_rank(selected, momentum, method = "exponential")data("sample_prices_weekly") momentum <- calc_momentum(sample_prices_weekly, lookback = 12) selected <- filter_top_n(momentum, 10) # Linear rank weighting (best gets most) weights <- weight_by_rank(selected, momentum, method = "linear") # Exponential (heavy on top stocks) weights_exp <- weight_by_rank(selected, momentum, method = "exponential")
Applies different weighting methods based on market regime classification. Enables adaptive strategies that change allocation approach in different market conditions.
weight_by_regime( selected_df, regime, weighting_configs, signal_df = NULL, vol_timeframe_data = NULL, strategy_timeframe_data = NULL )weight_by_regime( selected_df, regime, weighting_configs, signal_df = NULL, vol_timeframe_data = NULL, strategy_timeframe_data = NULL )
selected_df |
Binary selection matrix (1 = selected, 0 = not) |
regime |
Regime classification (integer values per period) |
weighting_configs |
List with method-specific parameters |
signal_df |
Signal values (required for signal/rank methods) |
vol_timeframe_data |
Volatility data (required for volatility method) |
strategy_timeframe_data |
Strategy timeframe alignment data |
Data.table with regime-adaptive weights
data("sample_prices_weekly") # Create selection and signals momentum <- calc_momentum(sample_prices_weekly, lookback = 12) selected <- filter_top_n(momentum, n = 10) # Create a simple regime (example: based on market trend) ma20 <- calc_moving_average(sample_prices_weekly, 20) spy_price <- sample_prices_weekly$SPY spy_ma <- ma20$SPY regime <- ifelse(spy_price > spy_ma, 1, 2) # Different weights for bull/bear markets weighting_configs <- list( "1" = list(method = "equal"), "2" = list(method = "signal") ) weights <- weight_by_regime(selected, regime, weighting_configs, signal_df = momentum)data("sample_prices_weekly") # Create selection and signals momentum <- calc_momentum(sample_prices_weekly, lookback = 12) selected <- filter_top_n(momentum, n = 10) # Create a simple regime (example: based on market trend) ma20 <- calc_moving_average(sample_prices_weekly, 20) spy_price <- sample_prices_weekly$SPY spy_ma <- ma20$SPY regime <- ifelse(spy_price > spy_ma, 1, 2) # Different weights for bull/bear markets weighting_configs <- list( "1" = list(method = "equal"), "2" = list(method = "signal") ) weights <- weight_by_regime(selected, regime, weighting_configs, signal_df = momentum)
Collection of risk-based weighting methods for portfolio construction. Each method allocates capital based on risk characteristics rather than market capitalization or equal weights.
weight_by_risk_parity( selected_df, prices_df, method = c("inverse_vol", "equal_risk", "max_div"), lookback_periods = 252, min_periods = 60 )weight_by_risk_parity( selected_df, prices_df, method = c("inverse_vol", "equal_risk", "max_div"), lookback_periods = 252, min_periods = 60 )
selected_df |
Binary selection matrix (data.frame with Date column). |
prices_df |
Price data for risk calculations (typically daily). Returns are calculated internally from prices. |
method |
Optimization method for risk parity. |
lookback_periods |
Number of periods for risk estimation (default: 252). |
min_periods |
Minimum periods required (default: 60). |
Methods
inverse_volWeights proportional to 1 / volatility
(e.g., 1 / sd of returns over lookback_periods). Lower volatility
stocks receive higher weights.
equal_riskEqual Risk Contribution (ERC): finds weights so each position contributes equally to total portfolio risk (iterative optimization).
max_divMaximum Diversification: maximizes the ratio of weighted average volatility to portfolio volatility.
The function accepts price data and calculates returns internally, ensuring consistency with other library functions. Daily prices are recommended for accurate volatility estimation.
Weight matrix with the same dates as selected_df; each row sums to 1.
data("sample_prices_daily") data("sample_prices_weekly") momentum <- calc_momentum(sample_prices_weekly, lookback = 12) selected <- filter_top_n(momentum, n = 10) weight_by_risk_parity(selected, sample_prices_daily, method = "inverse_vol") weight_by_risk_parity(selected, sample_prices_daily, method = "equal_risk") weight_by_risk_parity(selected, sample_prices_daily, method = "max_div")data("sample_prices_daily") data("sample_prices_weekly") momentum <- calc_momentum(sample_prices_weekly, lookback = 12) selected <- filter_top_n(momentum, n = 10) weight_by_risk_parity(selected, sample_prices_daily, method = "inverse_vol") weight_by_risk_parity(selected, sample_prices_daily, method = "equal_risk") weight_by_risk_parity(selected, sample_prices_daily, method = "max_div")
Weights selected securities proportionally to their signal strength. Stronger signals receive higher allocations.
weight_by_signal(selected_df, signal_df)weight_by_signal(selected_df, signal_df)
selected_df |
Binary selection matrix |
signal_df |
Signal values for weighting |
Data.table with signal-proportional weights
data("sample_prices_weekly") momentum <- calc_momentum(sample_prices_weekly, lookback = 12) selected <- filter_top_n(momentum, 10) # Weight by momentum strength weights <- weight_by_signal(selected, momentum)data("sample_prices_weekly") momentum <- calc_momentum(sample_prices_weekly, lookback = 12) selected <- filter_top_n(momentum, 10) # Weight by momentum strength weights <- weight_by_signal(selected, momentum)
Weights securities based on their volatility characteristics. Can prefer low-volatility (defensive) or high-volatility (aggressive) stocks.
weight_by_volatility( selected_df, vol_timeframe_data, strategy_timeframe_data = NULL, lookback_periods = 26, low_vol_preference = TRUE, vol_method = "std", weighting_method = c("rank", "equal", "inverse_variance") )weight_by_volatility( selected_df, vol_timeframe_data, strategy_timeframe_data = NULL, lookback_periods = 26, low_vol_preference = TRUE, vol_method = "std", weighting_method = c("rank", "equal", "inverse_variance") )
selected_df |
Binary selection matrix (1 = selected, 0 = not) |
vol_timeframe_data |
Price data for volatility calculation (usually daily) |
strategy_timeframe_data |
Price data matching strategy frequency |
lookback_periods |
Number of periods for volatility (default: 26) |
low_vol_preference |
TRUE = lower vol gets higher weight (default: TRUE) |
vol_method |
"std", "range", "mad", or "abs_return" |
weighting_method |
"rank", "equal", or "inverse_variance" |
Data.table with volatility-based weights
data("sample_prices_weekly") data("sample_prices_daily") momentum <- calc_momentum(sample_prices_weekly, lookback = 12) selected <- filter_top_n(momentum, 10) daily_vol <- calc_rolling_volatility(sample_prices_daily, lookback = 252) aligned_vol <- align_to_timeframe(daily_vol, sample_prices_weekly$Date) weights <- weight_by_volatility(selected, aligned_vol, low_vol_preference = TRUE)data("sample_prices_weekly") data("sample_prices_daily") momentum <- calc_momentum(sample_prices_weekly, lookback = 12) selected <- filter_top_n(momentum, 10) daily_vol <- calc_rolling_volatility(sample_prices_daily, lookback = 252) aligned_vol <- align_to_timeframe(daily_vol, sample_prices_weekly$Date) weights <- weight_by_volatility(selected, aligned_vol, low_vol_preference = TRUE)
Creates equal-weighted portfolio from selection matrix. The simplest and often most robust weighting scheme.
weight_equally(selected_df)weight_equally(selected_df)
selected_df |
Binary selection matrix (1 = selected, 0 = not) |
Data.table with equal weights for selected securities
data("sample_prices_weekly") momentum <- calc_momentum(sample_prices_weekly, lookback = 12) selected <- filter_top_n(momentum, 10) weights <- weight_equally(selected)data("sample_prices_weekly") momentum <- calc_momentum(sample_prices_weekly, lookback = 12) selected <- filter_top_n(momentum, 10) weights <- weight_equally(selected)
Map scores to portfolio weights
weight_from_scores( scores, method = c("softmax", "rank", "linear", "equal"), temperature = 10, floor = 0 )weight_from_scores( scores, method = c("softmax", "rank", "linear", "equal"), temperature = 10, floor = 0 )
scores |
Wide score panel (Date + symbols). |
method |
|
temperature |
softmax temperature (higher=flatter). |
floor |
non-negative number added before normalization. |
weight panel (Date + symbols) with non-negative rows summing to 1
over active dates.
Prints a concise summary of a wf_optimization_result: configuration,
stitched OOS performance, and parameter stability.
wf_report(wf, digits = 4)wf_report(wf, digits = 4)
wf |
A |
digits |
Integer; number of digits when printing numeric values (default 4). |
Invisibly returns the optimization summary data frame.
Concatenates OOS backtests and safely compounds returns on overlapping dates.
wf_stitch(oos_results, initial_value = 1e+05)wf_stitch(oos_results, initial_value = 1e+05)
oos_results |
List of backtest_result objects, each with $portfolio_values and $dates. |
initial_value |
Numeric starting value for the stitched equity curve (default 100000). |
Data frame with columns: Date, Value.
Walk-forward sweep of tabular configs (window-wise distribution of metrics)
wf_sweep_tabular( features_list, labels, prices, fit_fn, predict_fn, schedule = list(is = 104L, oos = 4L, step = 4L), grid = list(top_k = c(10L, 15L), temperature = c(8, 12), method = c("softmax", "rank"), transform = c("zscore")), caps = list(max_per_symbol = 0.08, max_per_group = NULL), group_map = NULL, freq = 52, cost_bps = 0, max_windows = NULL, ic_method = "spearman" )wf_sweep_tabular( features_list, labels, prices, fit_fn, predict_fn, schedule = list(is = 104L, oos = 4L, step = 4L), grid = list(top_k = c(10L, 15L), temperature = c(8, 12), method = c("softmax", "rank"), transform = c("zscore")), caps = list(max_per_symbol = 0.08, max_per_group = NULL), group_map = NULL, freq = 52, cost_bps = 0, max_windows = NULL, ic_method = "spearman" )
features_list |
List of feature panels. |
labels |
Label panel. |
prices |
Price panel for backtests. |
fit_fn, predict_fn
|
Model fit and predict functions. |
schedule |
List with elements is, oos, step. |
grid |
Named list of parameter vectors to sweep (e.g., top_k, temperature, method, transform). |
caps |
Exposure caps (e.g., max_per_symbol/max_per_group). |
group_map |
Optional Symbol->Group mapping for group caps/selection. |
freq |
Compounding frequency for annualization (e.g., 52 for weekly). |
cost_bps |
One-way trading cost in basis points (applied on rebalance). |
max_windows |
optional limit for speed. |
ic_method |
IC method ('spearman' or 'pearson'). |
data.table with medians/means across OOS windows.
Downloads stock price data directly from Yahoo Finance using quantmod. No database required - perfect for quick analysis and experimentation. Get started with real data in under 5 minutes.
yahoo_adapter(symbols, start_date, end_date, frequency = "daily")yahoo_adapter(symbols, start_date, end_date, frequency = "daily")
symbols |
Character vector of stock symbols |
start_date |
Start date in "YYYY-MM-DD" format |
end_date |
End date in "YYYY-MM-DD" format |
frequency |
"daily" or "weekly" (default: "daily") |
Data.table with Date column and one column per symbol
# Use included sample data data(sample_prices_weekly) # Build a quick momentum strategy with offline data momentum <- calc_momentum(sample_prices_weekly, lookback = 12) selected <- filter_top_n(momentum, n = 2) weights <- weight_equally(selected) result <- run_backtest(sample_prices_weekly, weights, initial_capital = 100000) # Download tech stocks (requires internet, skipped on CRAN) if (requireNamespace("quantmod", quietly = TRUE)) { prices <- yahoo_adapter( symbols = c("AAPL", "MSFT", "GOOGL"), start_date = "2023-01-01", end_date = "2023-12-31", frequency = "weekly" ) momentum <- calc_momentum(prices, lookback = 12) }# Use included sample data data(sample_prices_weekly) # Build a quick momentum strategy with offline data momentum <- calc_momentum(sample_prices_weekly, lookback = 12) selected <- filter_top_n(momentum, n = 2) weights <- weight_equally(selected) result <- run_backtest(sample_prices_weekly, weights, initial_capital = 100000) # Download tech stocks (requires internet, skipped on CRAN) if (requireNamespace("quantmod", quietly = TRUE)) { prices <- yahoo_adapter( symbols = c("AAPL", "MSFT", "GOOGL"), start_date = "2023-01-01", end_date = "2023-12-31", frequency = "weekly" ) momentum <- calc_momentum(prices, lookback = 12) }