Reusable research workflows for macro-financial and
cross-asset analysis. The package sits on top of data packages such
as 'investdatar' and focuses on transforming existing market and
macro data into analysis-ready research tables, factor and indicator
constructions, event and regime overlays, sensitivity estimates,
scenario comparisons, and research-grade visualization outputs.
Portfolio risk-return comparisons use the mean-variance framework of
Markowitz (1952)
investlabr is an R package for macro-financial and cross-asset research workflows.
It sits on top of data packages such as investdatar and provides reusable tools for:
In simple terms:
investdatar gets and stores the datastrategyr turns strategy logic into executable actionsinvestlabr thinks with the data and communicates the ideainvestlabr is not a raw data collection package and not a trading execution package.
Its role is the layer between:
This package is being expanded from a narrower strategy-evaluation toolkit into a broader research workflow package. Some current functions still reflect the earlier scope and will be migrated into a clearer module structure over time.
investlabr is not the package that should decide live strategy actions. That role belongs more naturally to strategyr, where dynamic strategy logic and execution-oriented backtesting live. investlabr is the looser and more exploratory layer for visualizing market patterns, stress-testing ideas, and communicating investment thinking to a human audience.
Examples of likely function families:
prep_*factor_*event_*regime_*sense_*sim_*viz_*brief_*Those concerns belong in adjacent packages such as investdatar, dedicated orchestration wrappers, or trading-specific packages.
strategyrstrategyr and investlabr are related, but they solve different problems.
strategyr is for timely investment decision-makingstrategyr owns realistic, path-dependent, execution-oriented backtestingstrategyr should be able to support executable orders or action selectioninvestlabr is for exploratory research, brainstorming, visualization, and human-facing idea sharinginvestlabr may keep lightweight or simplified backtesting when it helps explain a market pattern, but that is not its core identityinvestdatarinvestdatar owns:
investlabr owns:
The repository currently includes legacy functions centered on event evaluation, strategy performance review, portfolio analysis, and plotting. Those functions remain useful, but the package identity is now broader:
investlabris an R package for reusable macro, rates, liquidity, and cross-asset research workflows, including factor construction, event/regime analysis, sensitivity estimation, scenario simulation, and research-grade visualization.
# install.packages("remotes")
remotes::install_github("OliverLDS/investlabr")
data.table, ggplot2, quadprog, Rcppggrepel, patchwork, gridExtra, scalesPACKAGE_SCOPE.mdARCHITECTURE.mdNEWS.mdinst/gallery/README.mdscripts/README.mdoutput/README.mdPUBLISHING_CONTRACT.mdThe codebase is now organized by module family:
prep-*: research data preparation scaffoldsfactor-*: reusable factor and diagnostic buildersevent-*: event-study style analyticsregime-*: regime definitions and comparisons scaffoldssense-*: sensitivity and exposure scaffoldssim-*: simulation and legacy backtesting helpersviz-*: visualization and layout utilitiesbrief-*: briefing-oriented plots and summariesSome current exported functions still use older eval_* and gen_* names. Those remain for compatibility while the package migrates toward the new module naming convention.
investlabr now includes a shared visualization style layer so charts can use a more professional and varied visual language without rewriting each plotting function.
Current named styles include:
research_notemacro_classicterminal_riskcross_asset_colorminimal_printstrategy_explainpresentation_boldbriefing_serifinstitutional_bluepolicy_memodesk_monitorclient_slidenewswire_printCurrent contexts include:
reportslidedashboardYou can inspect or change the defaults with:
library(investlabr)
viz_style_get()
viz_style_set(style = "macro_classic", context = "slide")
Most plotting functions now accept style and context, for example:
gen_yield_curve_plot(curve_dt, style = "macro_classic", context = "slide")
gen_plot_event_tsline_cum_ret(evt, style = "presentation_bold", context = "report")
eval_strat_plot_tsline_eq(bt_res, style = "terminal_risk", context = "dashboard")
If INVESTLABR_CONFIG points to a YAML file with plot_compiler_name, viz_theme_apply() will append Compiled by: ... to plot captions by default. Higher-level wrappers such as gen_grid_of_plots_with_labels() and the patchwork-based market dashboard path also use the same compiler footer logic. You can suppress that per plot with show_compiler = FALSE.
If the compiler name contains special YAML characters such as @, :, #, or leading/trailing spaces, wrap the string in quotes.
plot_compiler_name: Oliver Zhou
plot_compiler_name: "@user_name"
For larger real-data workflows and style/context showcases, see the gallery in inst/gallery/README.md. The README examples below stay relatively compact on purpose.
library(data.table)
library(investlabr)
DT <- data.table(
datetime = seq.POSIXt(as.POSIXct("2024-01-01", tz = "UTC"), by = "day", length.out = 120),
close = cumprod(1 + rnorm(120, mean = 0.001, sd = 0.01)),
event_policy = as.integer(seq_len(120) %% 20 == 0)
)
evt <- eval_event_performance(DT, "event_policy", H = 1L:10L)
gen_plot_event_tsline_cum_ret(evt)
library(data.table)
library(investlabr)
DT <- data.table(
datetime = seq.POSIXt(as.POSIXct("2024-01-01", tz = "UTC"), by = "day", length.out = 150),
close = cumprod(1 + rnorm(150, mean = 0.0005, sd = 0.01)),
event_a = as.integer(seq_len(150) %% 25 == 0),
event_b = as.integer(seq_len(150) %% 40 == 0)
)
evt_a <- eval_event_performance(DT, "event_a", H = 1L:15L)
evt_b <- eval_event_performance(DT, "event_b", H = 1L:15L)
gen_plot_comparing_events(
evt_a, evt_b,
first_event_label = "Event A",
second_event_label = "Event B"
)
library(data.table)
library(investlabr)
yield_dt_list <- list(
data.table(date = as.Date(c("2024-01-01", "2024-02-01")), value = c(5.1, 5.0)),
data.table(date = as.Date(c("2024-01-01", "2024-02-01")), value = c(4.8, 4.7)),
data.table(date = as.Date(c("2024-01-01", "2024-02-01")), value = c(4.5, 4.4)),
data.table(date = as.Date(c("2024-01-01", "2024-02-01")), value = c(4.2, 4.1)),
data.table(date = as.Date(c("2024-01-01", "2024-02-01")), value = c(4.0, 3.9)),
data.table(date = as.Date(c("2024-01-01", "2024-02-01")), value = c(3.9, 3.8)),
data.table(date = as.Date(c("2024-01-01", "2024-02-01")), value = c(3.8, 3.7)),
data.table(date = as.Date(c("2024-01-01", "2024-02-01")), value = c(3.7, 3.6)),
data.table(date = as.Date(c("2024-01-01", "2024-02-01")), value = c(3.6, 3.5)),
data.table(date = as.Date(c("2024-01-01", "2024-02-01")), value = c(3.7, 3.6)),
data.table(date = as.Date(c("2024-01-01", "2024-02-01")), value = c(3.8, 3.7))
)
curve_dt <- get_yield_data_DT(
yield_dt_list,
yield_dates = list(
Now = as.Date("2024-02-01"),
`One month ago` = as.Date("2024-01-01")
)
)
gen_yield_curve_plot(curve_dt)
This is the lightweight and communication-oriented backtesting layer that remains in investlabr. More realistic and execution-grade strategy logic should live in strategyr.
library(data.table)
library(investlabr)
DT <- data.table(
datetime = seq.POSIXt(as.POSIXct("2024-01-01", tz = "UTC"), by = "day", length.out = 120),
open = 100 + cumsum(rnorm(120, 0.1, 1)),
close = 100 + cumsum(rnorm(120, 0.1, 1)),
pos_demo = rep(c(0, 1, 1, 0, -1, -1), length.out = 120)
)
attr(DT, "inst_id") <- "DEMO-ASSET"
attr(DT$pos_demo, "strat_name") <- "demo_strategy"
attr(DT$pos_demo, "strat_par") <- list(window = 20)
bt_res <- eval_strat_performance(DT, "pos_demo")
eval_strat_plot_tsline_eq(bt_res)
eval_strat_plot_scatter_maxdd_annret(bt_res)
If you want multiple rows for the scatter plot, bind several backtest results first:
bt_res_list <- data.table::rbindlist(list(bt_res, bt_res))
eval_strat_plot_scatter_maxdd_annret(bt_res_list)
library(data.table)
library(investlabr)
mk_bt_res <- function(asset_name, seed) {
set.seed(seed)
DT <- data.table(
datetime = seq.POSIXt(as.POSIXct("2024-01-01", tz = "UTC"), by = "day", length.out = 120),
open = 100 + cumsum(rnorm(120, 0.1, 1)),
close = 100 + cumsum(rnorm(120, 0.1, 1)),
pos_demo = rep(c(0, 1, 1, 0, -1, -1), length.out = 120)
)
attr(DT, "inst_id") <- asset_name
attr(DT$pos_demo, "strat_name") <- "demo_strategy"
attr(DT$pos_demo, "strat_par") <- list(window = 20)
eval_strat_performance(DT, "pos_demo")
}
bt_a <- mk_bt_res("DEMO-ASSET-A", seed = 1)
bt_b <- mk_bt_res("DEMO-ASSET-B", seed = 2)
bt_res_list <- data.table::rbindlist(list(bt_a, bt_b), fill = TRUE)
# Keep the target feasible for the sample inputs.
target_return <- min(bt_res_list$annual_return) * 0.8
weight_res <- get_optimal_weights(bt_res_list, target_return = target_return)
port_res <- eval_portfolio_performance(bt_res_list, target_return = target_return)
weight_res$optimal_weights_table
eval_strat_plot_tsline_eq(port_res)
library(data.table)
library(investlabr)
set.seed(1)
n <- 40
datetime <- seq.POSIXt(as.POSIXct("2024-01-01", tz = "UTC"), by = "day", length.out = n)
open <- 100 + cumsum(rnorm(n, 0.1, 1))
close <- open + rnorm(n, 0, 1)
upper_wick <- abs(rnorm(n, 0.8, 0.3))
lower_wick <- abs(rnorm(n, 0.8, 0.3))
DT <- data.table(
datetime = datetime,
open = open,
high = pmax(open, close) + upper_wick,
low = pmin(open, close) - lower_wick,
close = close
)
gen_candle_plots_with_sr_lines(
DT,
support_pts = c(95, 97.5),
resistance_pts = c(108, 112)
)
library(data.table)
library(investlabr)
set.seed(1)
DT <- data.table(
date = seq.Date(as.Date("2024-01-01"), by = "day", length.out = 150),
close = 100 + cumsum(rnorm(150, 0.05, 1)),
event_a = as.integer(seq_len(150) %% 25 == 0),
event_b = as.integer(seq_len(150) %% 40 == 0)
)
p1 <- gen_plot_event_tsline_cum_ret(
eval_event_performance(DT, "event_a", H = 1L:15L),
show_compiler = FALSE
)
p2 <- gen_plot_event_tsline_cum_ret(
eval_event_performance(DT, "event_b", H = 1L:15L),
show_compiler = FALSE
)
gen_grid_of_plots_with_labels(
plots = list(p1, p2),
n_rows = 1,
n_cols = 2,
col_labs = c("Event A", "Event B"),
title = "Event Comparison"
)
This example uses investdatar and Yahoo Finance data to build a 2 by 2 cross-asset event board. Equity, bond, FX, and commodity each get one subplot, and the symbols within each asset class are overlaid with different colors. Each series is indexed to 100 on the last trading day on or before the event date so the post-event paths are directly comparable inside each panel.
library(data.table)
library(ggplot2)
library(investdatar)
library(investlabr)
event_date <- as.Date("2026-02-28")
event_label <- "Feb 28, 2026 (Iran War)"
from_date <- "2026-01-15"
to_date <- "2026-04-01"
groups <- list(
Equity = c("^GSPC", "^STOXX50E", "^N225", "^HSI"),
Bond = c("ZT=F", "HYG", "LQD"),
FX = c("DX-Y.NYB", "EURUSD=X", "GBPUSD=X"),
Commodity = c("GC=F", "HG=F", "CL=F")
)
all_symbols <- unlist(groups, use.names = FALSE)
invisible(lapply(
all_symbols,
function(sym) investdatar::sync_local_quantmod_OHLC(
ticker = sym,
label = sym,
from = from_date,
to = to_date,
src = "yahoo"
)
))
make_asset_panel <- function(symbols, panel_title) {
dt_list <- lapply(symbols, function(symbol) {
dt <- investdatar::get_local_quantmod_OHLC(symbol, src = "yahoo")
dt <- data.table::as.data.table(dt)[date >= as.Date(from_date) & date <= as.Date(to_date)]
event_ref_date <- max(dt$date[dt$date <= event_date])
event_ref_close <- dt[date == event_ref_date, close][1]
dt[, `:=`(
symbol = symbol,
event_ref_date = event_ref_date,
index100 = close / event_ref_close * 100
)]
dt
})
panel_dt <- data.table::rbindlist(dt_list, fill = TRUE)
subtitle_date <- panel_dt[, min(event_ref_date)]
last_labels <- panel_dt[!is.na(index100), .SD[.N], by = symbol]
p <- ggplot(panel_dt, aes(x = datetime, y = index100, color = symbol)) +
geom_line(linewidth = 0.9) +
geom_text(
data = last_labels,
aes(label = symbol),
hjust = -0.1,
vjust = 0.5,
show.legend = FALSE,
size = 3.2
) +
labs(
title = panel_title,
subtitle = paste0("Indexed to 100 on ", format(subtitle_date, "%Y-%m-%d")),
x = "",
y = ""
)
p <- viz_theme_apply(
p,
style = "macro_classic",
context = "report",
legend_position = "bottom",
show_compiler = FALSE
)
p <- viz_annotate_event_lines(
p,
data.table::data.table(datetime = as.POSIXct(event_date), label = event_label),
x_col = "datetime",
label_col = "label",
style = "macro_classic",
context = "report"
)
p
}
equity_plot <- make_asset_panel(groups$Equity, "Equity")
bond_plot <- make_asset_panel(groups$Bond, "Bond")
fx_plot <- make_asset_panel(groups$FX, "FX")
commodity_plot <- make_asset_panel(groups$Commodity, "Commodity")
gen_grid_of_plots_with_labels(
plots = list(
equity_plot, bond_plot,
fx_plot, commodity_plot
),
n_rows = 2,
n_cols = 2,
title = "Cross-Asset Performance After Feb 28, 2026",
bottom = "Each series is indexed to 100 at the last trading date on or before the event date."
)
investdatarinvestlabr assumes data has already been retrieved or synchronized elsewhere. A typical workflow is:
# library(investdatar)
# library(investlabr)
#
# fred_local <- investdatar::get_local_FRED_data("DGS10")
# curve_inputs <- ...
# research_tbl <- investlabr::get_yield_data_DT(curve_inputs, yield_dates = ...)
investdatarThe example below shows the intended workflow boundary directly: investdatar reads the local FRED yield series, and investlabr converts those series into a research-ready yield-curve comparison chart.
library(investdatar)
library(investlabr)
series_ids <- c(
"DGS1MO", "DGS3MO", "DGS6MO", "DGS1", "DGS2", "DGS3",
"DGS5", "DGS7", "DGS10", "DGS20", "DGS30"
)
# Optional if you have not synced these series locally yet:
invisible(lapply(series_ids, investdatar::sync_local_fred_data))
yield_dt_list <- lapply(series_ids, investdatar::get_local_FRED_data)
curve_dt <- investlabr::get_yield_data_DT(
yield_dt_list = yield_dt_list,
yield_dates = list(
Now = Sys.Date(),
`One month ago` = Sys.Date() - 30,
`One year ago` = Sys.Date() - 365
)
)
investlabr::gen_yield_curve_plot(
curve_dt,
selected_windows = c("Now", "One month ago", "One year ago")
)
investdatarThis example uses investdatar as the Yahoo Finance access layer and investlabr as the charting layer.
library(data.table)
library(investdatar)
library(investlabr)
investdatar::sync_local_quantmod_OHLC(
ticker = "SPY",
from = "2024-01-01",
to = "2024-12-31",
src = "yahoo"
)
spy_dt <- investdatar::get_local_quantmod_OHLC("SPY", src = "yahoo")
investlabr::gen_candle_plots_with_sr_lines(
data.table::as.data.table(spy_dt)[1:60, .(datetime, open, high, low, close)],
support_pts = c(500, 520),
resistance_pts = c(560, 580)
)
These examples show the intended package boundary:
investdatar handles data syncing and local accessinvestlabr turns those series into reusable research objects and chartsstrategyr is where dynamically adaptive, execution-grade strategy logic should livedata.table, tidy tables, ggplot objects, or small named lists.strategyr.investlabr now includes a basic testthat setup for smoke coverage of package-level workflows.
# install.packages("testthat")
testthat::test_dir("tests/testthat")
MIT © 2025 Oliver Zhou