ledgr is an event-sourced systematic trading research framework for R.
Use it when you want a backtest result to be more than a temporary object in an R session. ledgr starts from sealed market-data snapshots, runs strategies through an experiment boundary, records event-sourced results, and lets you reopen the evidence later.
sealed snapshot -> experiment -> run -> event ledger -> results
The setup is not overhead. The setup is the audit trail.
ledgr is research software, not investment advice. Backtests and audit trails are evidence tools; they do not predict future returns or provide compliance guarantees. See DISCLAIMER.md.
if (!requireNamespace("pak", quietly = TRUE)) install.packages("pak")
pak::pak("blechturm/ledgr")library(ledgr)
library(dplyr)
data("ledgr_demo_bars", package = "ledgr")Start with the package-owned demo bars. Real research should seal your own market data, but the demo data keeps this first run local and deterministic.
bars <- ledgr_demo_bars |>
filter(
instrument_id %in% c("DEMO_01", "DEMO_02"),
between(ts_utc, ledgr_utc("2019-01-01"), ledgr_utc("2019-06-30"))
)
bars |>
slice_head(n = 4)
#> # A tibble: 4 x 7
#> ts_utc instrument_id open high low close volume
#> <dttm> <chr> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 2019-01-01 00:00:00 DEMO_01 89.7 91.8 89.7 91.5 468600
#> 2 2019-01-02 00:00:00 DEMO_01 91.5 91.6 91.0 91.3 438315
#> 3 2019-01-03 00:00:00 DEMO_01 91.3 92.1 89.6 90.5 576390
#> 4 2019-01-04 00:00:00 DEMO_01 90.7 91.1 89.5 89.8 458921Seal the bars, declare the strategy boundary, and run one parameter set.
snapshot <- ledgr_snapshot_from_df(
bars,
snapshot_id = "readme_demo"
)
features <- ledgr_feature_map(
fast = ledgr_ind_sma(ledgr_param("fast_n")),
slow = ledgr_ind_sma(ledgr_param("slow_n"))
)
exp <- ledgr_experiment(
snapshot = snapshot,
strategy = ledgr_demo_sma_crossover_strategy(),
features = features,
opening = ledgr_opening(cash = 10000),
cost_model = ledgr_cost_zero()
)
bt <- ledgr_run(
exp,
feature_params = list(fast_n = 10L, slow_n = 40L),
params = list(qty = 10, threshold = 0),
run_id = "readme_sma_crossover"
)
summary(bt)
#> ledgr Backtest Summary
#> ======================
#>
#> Performance Metrics:
#> Total Return: 1.07%
#> Annualized Return: 2.11%
#> Max Drawdown: -0.76%
#>
#> Risk Metrics:
#> Risk-Free Rate: 0.00% annual
#> Annualization: 252 periods/year (US equity daily)
#> Volatility (annual): 1.56%
#> Sharpe Ratio: 1.349
#>
#> Trade Statistics:
#> Closed Trades: 2
#> Win Rate: 100.00%
#> Avg Trade: $53.41
#>
#> Exposure:
#> Time in Market: 59.69%The result views are derived from recorded events. The ledger is the source of truth; trades, equity, and metrics are views over that evidence.
ledgr_results(bt, what = "trades")
#> # A tibble: 2 x 9
#> event_seq ts_utc instrument_id side qty price fee realized_pnl action
#> <int> <date> <chr> <chr> <dbl> <dbl> <dbl> <dbl> <chr>
#> 1 3 2019-04-23 DEMO_01 SELL 10 102. 0 27.4 CLOSE
#> 2 4 2019-06-13 DEMO_02 SELL 10 76.5 0 79.4 CLOSE
head(ledgr_results(bt, what = "equity"), 3)
#> # A tibble: 3 x 6
#> ts_utc equity cash positions_value running_max drawdown
#> <date> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 2019-01-01 10000 10000 0 10000 0
#> 2 2019-01-02 10000 10000 0 10000 0
#> 3 2019-01-03 10000 10000 0 10000 0
head(ledgr_results(bt, what = "returns"), 3)
#> # A tibble: 3 x 3
#> ts_utc equity period_return
#> <date> <dbl> <dbl>
#> 1 2019-01-01 10000 NA
#> 2 2019-01-02 10000 0
#> 3 2019-01-03 10000 0Stored strategy provenance is inspectable without rerunning or
evaluating the strategy source. Use trust = FALSE for source and
metadata inspection.
stored_strategy <- ledgr_run_strategy(snapshot, "readme_sma_crossover", trust = FALSE)
list(
reproducibility_level = stored_strategy$reproducibility_level,
hash_verified = stored_strategy$hash_verified,
strategy_params = stored_strategy$strategy_params
)
#> $reproducibility_level
#> [1] "tier_1"
#>
#> $hash_verified
#> [1] TRUE
#>
#> $strategy_params
#> $strategy_params$qty
#> [1] 10
#>
#> $strategy_params$threshold
#> [1] 0Hash verification proves stored-text identity, not code safety. Use
trust = TRUE only when you already trust the store and intentionally
want to recover a function object.
ledgr_target is a thin wrapper around the named numeric quantities the
fold consumes. Inspect one quantity with ordinary [[ indexing, or
recover the full named numeric vector with c().
target <- ledgr_target(
c(DEMO_01 = 10, DEMO_02 = 0),
universe = c("DEMO_01", "DEMO_02")
)
target[["DEMO_01"]]
#> [1] 10
target_values <- c(target)
target_values
#> DEMO_01 DEMO_02
#> 10 0An exploratory sweep remains evidence, not a decision. Name the ranking rule, inspect its presentation table, then extract a candidate from the full ranked table before promotion.
grid <- ledgr_grid_cross(
features = ledgr_feature_grid(
fast_n = c(10L, 20L),
slow_n = 40L
),
strategy = ledgr_strategy_grid(
qty = c(5, 10),
threshold = 0
)
)
sweep <- ledgr_sweep(exp, grid, seed = 2026L)
review <- ledgr_sweep_review(sweep, rank_by = -final_equity, n = 2L)
review$top
#> # A tibble: 2 x 12
#> rank candidate_id candidate_row status final_equity total_return sharpe_ratio
#> <int> <chr> <int> <chr> <dbl> <dbl> <dbl>
#> 1 1 feature_9f9d160d8a33/~ 4 DONE 10109. 0.0109 1.39
#> 2 2 feature_fa560ccbec9f/~ 2 DONE 10107. 0.0107 1.35
#> # i 5 more variables: max_drawdown <dbl>, n_trades <int>, execution_seed <int>,
#> # params <list>, feature_params <list>
candidate <- ledgr_candidate(review$ranked, 1L)
promoted <- ledgr_promote(
exp,
candidate,
run_id = "readme_promoted_candidate"
)The run handle is a locator for durable evidence. Closing it releases owned resources; it does not delete the run. Keep the store path and stable IDs, then reopen the snapshot and completed run in a later session without executing the strategy again.
store_path <- snapshot$db_path
snapshot_id <- snapshot$snapshot_id
promoted_run_id <- promoted$run_id
close(bt)
close(promoted)
ledgr_snapshot_close(snapshot)
snapshot <- ledgr_snapshot_open(store_path, snapshot_id, verify = TRUE)
bt <- ledgr_run_open(snapshot, promoted_run_id)
head(ledgr_results(bt, what = "equity"), 3)
#> # A tibble: 3 x 6
#> ts_utc equity cash positions_value running_max drawdown
#> <date> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 2019-01-01 10000 10000 0 10000 0
#> 2 2019-01-02 10000 10000 0 10000 0
#> 3 2019-01-03 10000 10000 0 10000 0| Question | Article |
|---|---|
| I want the full research loop: snapshot, sweep, promotion, reopen. | Research Workflow |
| I want the shortest runnable path through the package. | Quickstart |
| I want to write strategies correctly. | Strategy Development |
| I want feature maps, indicators, and active aliases. | Indicators |
| I want point-in-time universes, missing-session handling, and durable explanations. | Survivorship Bias |
| I want exploratory sweeps and candidate promotion. | Sweeps |
| I want return-panel evidence, PBO/CSCV, MinTRL, K-Ratio, DSR, and effective-trial diagnostics. | Selection Integrity |
| I want cost and target-risk policy boundaries. | Risk And Cost |
| I want walk-forward evaluation. | Walk-Forward |
| I want sealed snapshots, durable stores, backup, and reopen. | Experiment Store |
| I want hashes, provenance tiers, and limits of recovery. | Reproducibility |
| I want fills, trades, equity, metrics, and metric context. | Metrics And Accounting |
Start with the pkgdown site for the full article set: https://blechturm.github.io/ledgr/.
Installed package help remains available from R:
help(package = "ledgr")
vignette(package = "ledgr")ledgr connects to the R finance ecosystem through adapters. The core is
narrow by design:
data -> pulse -> decision -> fill -> ledger event -> portfolio state.
Everything outside that sequence, such as data vendors, indicators,
charting, and analytics, can be provided by packages that already do
those things well.
| ledgr owns | Other packages can own |
|---|---|
| sealed snapshots and hashes | market-data acquisition |
| pulse construction and no-lookahead contexts | indicator calculations through adapters |
| target validation, target-risk transforms, fills, and ledger events | charting and visualization |
| run identity, provenance, and result reconstruction | downstream analytics and reporting |
This posture is deliberate. If you want an all-in-one charting or array-backtesting package, ledgr may not be the shortest path. Choose ledgr when you want the audit trail and adapter boundary to be explicit.
The current ledgr research API is experiment-first. It includes memory-backed
exploratory sweep support, compact saved sweeps with optional retained return
and closed-trade evidence, classed target-risk transforms, optional parallel
candidate dispatch, canonical single-run returns, public return panels,
evidence-only selection-integrity diagnostics, classed/hashable business
objectives, and all-candidates eligibility tear-downs. It also includes a scoped
compiled_accounting_model = "spot_fifo" opt-in for memory-backed spot-asset
FIFO sweeps. Canonical R execution remains the default.
The compiled opt-in is not durable ledgr_run() integration, not a non-spot
accounting model, and not a general compiled fold core. The target-risk layer
is a target-vector transformation layer; it is not affordability enforcement,
portfolio optimization, margin, shorting or borrow policy, liquidity/capacity
modeling, OMS lifecycle behavior, or broker-grade risk control. The
selection-integrity diagnostics and business-objective eligibility results do
not choose or promote candidates and do not prove future profitability.
business_objective_hash is evidence provenance in this release, not run,
sweep, candidate, promotion, session, or walk-forward identity. ledgr does not
ship ledgr_tune(), automatic objective-based selection, objective-filtered
walk-forward identity, scored objective composition, purging/embargo/CPCV,
benchmark-relative diagnostics, broker adapters, paper trading, or live
trading. Those are separate roadmap items with different state and safety
requirements.
ledgr_run() returns a live handle. The run artifacts are already
durable when the run finishes. Most result inspection opens and closes
its own read connection; explicit close(bt) is resource cleanup for
long sessions and explicit opens.
ledgr is not yet on CRAN. Until the first CRAN release, stored artifacts, database schemas, config hashes, provenance formats, and experimental APIs may change without backward compatibility or a deprecation cycle. Treat pre-CRAN ledgr as a research/development package and expect to rerun experiments after upgrading. Once ledgr is released on CRAN, the project will define an explicit compatibility and deprecation policy.