You want the smallest useful ledgr loop before reading the tool chapters.
This quickstart creates a sealed snapshot, runs one strategy, sweeps a small grid, and extracts one candidate for review. It is not a validation protocol and it is not a production deployment path. It is the shortest useful path from demo data to inspectable evidence.
Load A Small Dataset
Use two demo instruments over the first half of 2019. The code writes to a temporary DuckDB store so the rendered article leaves no project artifacts behind.
Declare The Experiment
The strategy reads stable aliases named fast and slow. The concrete SMA windows can vary later in the sweep.
features <- ledgr_feature_map(
fast = ledgr_ind_sma(ledgr_param("fast_n")),
slow = ledgr_ind_sma(ledgr_param("slow_n"))
)
strategy <- ledgr_demo_sma_crossover_strategy()
exp <- ledgr_experiment(
snapshot = snapshot,
strategy = strategy,
features = features,
opening = ledgr_opening(cash = 10000),
cost_model = ledgr_cost_zero()
)cost_model = ledgr_cost_zero() is explicit. If you want modeled costs, use a cost model such as ledgr_cost_spread_bps() or ledgr_cost_notional_bps_fee() rather than hiding costs inside the strategy.
Run One Case
Run one ordinary parameter set before sweeping. This catches setup mistakes while the result is still easy to inspect.
single_run <- ledgr_run(
exp,
params = list(qty = 5, threshold = 0),
feature_params = list(fast_n = 5L, slow_n = 20L),
run_id = "quickstart_run",
seed = 2026L
)
#> Warning: no DISPLAY variable so Tk is not available
summary(single_run)
#> ledgr Backtest Summary
#> ======================
#>
#> Performance Metrics:
#> Total Return: 0.24%
#> Annualized Return: 0.47%
#> Max Drawdown: -0.53%
#>
#> Risk Metrics:
#> Risk-Free Rate: 0.00% annual
#> Annualization: 252 periods/year (US equity daily)
#> Volatility (annual): 0.87%
#> Sharpe Ratio: 0.546
#>
#> Trade Statistics:
#> Total Trades: 6
#> Win Rate: 33.33%
#> Avg Trade: $4.01
#>
#> Exposure:
#> Time in Market: 67.44%
head(ledgr_results(single_run, what = "fills"), 3)
#> # A tibble: 3 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 1 2019-01-29 DEMO_01 BUY 5 91.9 0 0 OPEN
#> 2 2 2019-02-22 DEMO_02 BUY 5 69.5 0 0 OPEN
#> 3 3 2019-02-28 DEMO_02 SELL 5 67.3 0 -11.1 CLOSEIf this run has no fills, impossible prices, or surprising exposure, stop here. Sweeps amplify a setup; they do not repair it.
Sweep A Tiny Grid
Build feature parameters and strategy parameters separately, then cross them into candidate rows.
feature_grid <- ledgr_feature_grid(
fast_n = c(5L, 10L),
slow_n = c(20L, 40L),
.filter = fast_n < slow_n
)
strategy_grid <- ledgr_strategy_grid(
qty = 5,
threshold = c(0, 0.01)
)
grid <- ledgr_grid_cross(features = feature_grid, strategy = strategy_grid)
sweep <- ledgr_sweep(exp, grid, seed = 2026L)
sweep |>
select(candidate_id, status, total_return, sharpe_ratio) |>
arrange(desc(sharpe_ratio))
#> # ledgr sweep -- sweep_eaeda35a69e2b2c5
#> # A tibble: 8 x 4
#> candidate_id status sharpe_ratio total_return
#> <chr> <chr> <dbl> <chr>
#> 1 feature_9a29b31dae19/strategy_7ccbbefd14d1 DONE 3.08 +1.1%
#> 2 feature_6ff6fe3a1d38/strategy_7ccbbefd14d1 DONE 2.13 +0.8%
#> 3 feature_af0f94c90243/strategy_7ccbbefd14d1 DONE 2.06 +0.7%
#> 4 feature_af0f94c90243/strategy_86be010cf688 DONE 1.80 +0.8%
#> 5 feature_6ff6fe3a1d38/strategy_86be010cf688 DONE 1.38 +0.6%
#> 6 feature_fa560ccbec9f/strategy_86be010cf688 DONE 1.34 +0.5%
#> 7 feature_fa560ccbec9f/strategy_7ccbbefd14d1 DONE 1.30 +0.5%
#> 8 feature_9a29b31dae19/strategy_86be010cf688 DONE 0.546 +0.2%
#>
#> # i 8 combinations: 8 done, 0 failed.
#> # i Retention returns: none.
#> # i Snapshot hash: 6eeff5ca520c516a61e0228c5ac06d22548c9d74e4e98d1e9f71fccdd2b8a87e.
#> # i Cost model hash: 4011132b5979fc370e524ebbc525ac7f4158b4de43639ec985f4c90969b4b9d0.
#> # i Metric context hash: 794b69bd7f9c704447d4b0208b8420cdf132ec7bd6582eaa037bf1066133c1bb.
#> # i Saved artifact: not saved.
#> # i Rows are printed in their current table order; rank or arrange explicitly before selecting candidates.
#> # i Hidden columns (0):The sweep table is evidence, not an automatic recommendation. If you rank rows, make the ranking rule visible at the call site.
candidate <- ledgr_candidate(
sweep |> arrange(desc(sharpe_ratio)),
1L
)
candidate$candidate_id
#> [1] "feature_9a29b31dae19/strategy_7ccbbefd14d1"Promotion is the step that turns one selected candidate into a durable run. In a real project, use a deliberate run_id and a note that explains why this row was selected.
promoted <- ledgr_promote(
exp,
candidate,
run_id = "quickstart_promoted",
note = "Highest Sharpe ratio in the tiny demo sweep."
)Where Next
Read vignette("research-workflow", package = "ledgr") for the full project loop. Read vignette("sweeps", package = "ledgr") when the grid itself is the thing you need to understand. Read vignette("walk-forward", package = "ledgr") when you are ready to separate train-window selection from test-window evaluation.