https://github.com/blechturm/ledgr
Deterministic, event-sourced EOD backtesting for R. Sealed market-data snapshots and an append-only trade ledger give every experiment a reproducible identity.
https://github.com/blechturm/ledgr
backtesting backtesting-frameworks data-science duckdb reproducible-research trading
Last synced: about 1 month ago
JSON representation
Deterministic, event-sourced EOD backtesting for R. Sealed market-data snapshots and an append-only trade ledger give every experiment a reproducible identity.
- Host: GitHub
- URL: https://github.com/blechturm/ledgr
- Owner: blechturm
- License: other
- Created: 2025-12-18T21:08:28.000Z (7 months ago)
- Default Branch: main
- Last Pushed: 2026-06-03T00:56:39.000Z (about 1 month ago)
- Last Synced: 2026-06-03T01:12:46.472Z (about 1 month ago)
- Topics: backtesting, backtesting-frameworks, data-science, duckdb, reproducible-research, trading
- Language: R
- Homepage: https://blechturm.github.io/ledgr/
- Size: 6.07 MB
- Stars: 1
- Watchers: 0
- Forks: 1
- Open Issues: 1
-
Metadata Files:
- Readme: README.Rmd
- Changelog: NEWS.md
- License: LICENSE
- Agents: AGENTS.md
Awesome Lists containing this project
README
---
output:
github_document:
toc: false
html_preview: false
md_extensions: -smart
---
```{r setup, include=FALSE}
knitr::opts_chunk$set(
collapse = TRUE,
comment = "#>",
fig.path = "man/figures/README-",
out.width = "100%"
)
options(width = 90)
options(cli.unicode = FALSE)
if (file.exists("DESCRIPTION") && requireNamespace("pkgload", quietly = TRUE)) {
pkgload::load_all(".", quiet = TRUE)
}
default_output_hook <- knitr::knit_hooks$get("output")
knitr::knit_hooks$set(
output = function(x, options) {
x <- gsub("Database:(\\s+)[^\\r\\n]+[.]duckdb", "Database:\\1", x, perl = TRUE)
x <- gsub("[ \t]+(?=\n)", "", x, perl = TRUE)
default_output_hook(x, options)
}
)
```
# ledgr

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.
```text
sealed snapshot -> experiment -> run -> event ledger -> results
```
The setup is not overhead. The setup is the audit trail.
## Install
```{r install, eval=FALSE}
if (!requireNamespace("pak", quietly = TRUE)) install.packages("pak")
pak::pak("blechturm/ledgr")
```
```{r attach, message=FALSE}
library(ledgr)
library(dplyr)
data("ledgr_demo_bars", package = "ledgr")
```
## Run A Small Backtest
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.
```{r bars}
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)
```
Seal the bars, declare the strategy boundary, and run one parameter set.
```{r quick-run}
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)
)
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)
```
## Inspect The Evidence
The result views are derived from recorded events. The ledger is the source of
truth; trades, equity, and metrics are views over that evidence.
```{r inspect}
ledgr_results(bt, what = "trades")
head(ledgr_results(bt, what = "equity"), 3)
```
Stored strategy provenance is inspectable without rerunning or evaluating the
strategy source. Use `trust = FALSE` for source and metadata inspection.
```{r extract-strategy}
stored_strategy <- ledgr_extract_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
)
```
Hash 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.
## Where To Go Next
| Question | Article |
| --- | --- |
| I want the full research loop: snapshot, sweep, promotion, reopen. | [Research Workflow](https://blechturm.github.io/ledgr/articles/research-workflow.html) |
| I want to write strategies correctly. | [Strategy Development](https://blechturm.github.io/ledgr/articles/strategy-development.html) |
| I want feature maps, indicators, and active aliases. | [Indicators](https://blechturm.github.io/ledgr/articles/indicators.html) |
| I want exploratory sweeps and candidate promotion. | [Sweeps](https://blechturm.github.io/ledgr/articles/sweeps.html) |
| I want sealed snapshots, durable stores, backup, and reopen. | [Experiment Store](https://blechturm.github.io/ledgr/articles/experiment-store.html) |
| I want hashes, provenance tiers, and limits of recovery. | [Reproducibility](https://blechturm.github.io/ledgr/articles/reproducibility.html) |
| I want fills, trades, equity, metrics, and metric context. | [Metrics And Accounting](https://blechturm.github.io/ledgr/articles/metrics-and-accounting.html) |
Start with the pkgdown site for the full article set:
.
Installed package help remains available from R:
```{r docs, eval=FALSE}
help(package = "ledgr")
vignette(package = "ledgr")
```
## Ecosystem
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, 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.
## Scope
The current ledgr research API is experiment-first and includes sequential
exploratory sweep support. It does not ship automatic ranking, `ledgr_tune()`,
parallel sweep, walk-forward/PBO/CSCV helpers, full sweep artifact persistence,
broker adapters, paper trading, live trading, or short-selling semantics. 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,
explicit opens, and lazy result cursors.
```{r cleanup, include=FALSE}
close(bt)
ledgr_snapshot_close(snapshot)
```
## Pre-CRAN Compatibility
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.