Your strategy needs a moving average, a return, or an RSI at each decision. By the end of this article you can declare those features once, predict their IDs and warmup, and read their pulse-known values inside a strategy without ever reaching into future data.
An indicator is a declared feature computation. ledgr computes indicators into pulse-known values; scalar accessors, mapped accessors, long tables, and wide tables are different views of that same pulse-known data. That model is the same for built-in ledgr indicators, TTR-backed indicators, and custom indicators.
Prerequisites
Start With Built-In Features
Use two demo instruments and two built-in features. The economic idea will be small on purpose:
Own an instrument only when its recent return is positive enough and today’s close is above its moving average.
That rule needs one momentum feature and one trend feature. A feature map gives your strategy code readable aliases while preserving ledgr’s exact engine feature IDs.
features <- ledgr_feature_map(
ret_5 = ledgr_ind_returns(5),
sma_10 = ledgr_ind_sma(10)
)A feature ID is ledgr’s engine-facing name for a computed value. An alias is the strategy-facing name you choose in a feature map. Use aliases such as ret_5 or sma_10 when you write strategy logic with ctx$features(). Use feature IDs such as return_5 or sma_10 when you need the explicit engine contract, for example with ctx$feature() or when inspecting stored feature metadata.
Feature objects appear in three registration and inspection places:
| Surface | Accepted feature shape | How names are used |
|---|---|---|
ledgr_experiment(features = ...) |
indicator, list, named list, or feature map | registers feature definitions for the run |
ledgr_feature_contracts() / ledgr_feature_contract_check()
|
static indicator, list, named list, or feature map | reports aliases and engine IDs |
ledgr_pulse_snapshot(features = ...) |
static list or feature map | computes pulse-known values for inspection |
The strategy context then exposes the computed values through accessors:
| Accessor | Name type | Use |
|---|---|---|
ctx$vec$feature(feature_id) |
active alias or engine feature ID | reads one feature across the whole decision axis |
ctx$feature(id, feature_id) |
active alias or engine feature ID | reads one scalar value |
ctx$features(id, feature_map) |
feature map | returns a named vector keyed by alias |
The canonical strategy workflow is: register features on ledgr_experiment(), resolve the engine ID once, then read the whole decision axis with ctx$vec$feature(). Keep ctx$feature() and ctx$features() for one-instrument inspection and for active aliases, whose concrete engine IDs vary by candidate.
Feature Lifecycle: From Declaration To Lookup
The feature path has five steps:
flowchart LR declare["declare<br/>indicator or map"] register["register<br/>experiment"] compute["compute<br/>pulse-known values"] access["access<br/>ctx feature methods"] decide["decide<br/>target holdings"] declare --> register --> compute --> access --> decide
- You declare features in any of several shapes: individual indicators, built-in helpers, TTR adapters, CSV/R adapters, feature maps, or active-alias parameterizations. All resolve to the same lifecycle below.
-
ledgr_experiment()stores the declaration. Static lists and feature maps are ready immediately. Active-alias features are materialized for concrete feature-grid values before candidate execution. - Optional
ledgr_precompute_features()resolves a parameter grid, computes each candidate’s concrete feature set, deduplicates shared indicator fingerprints, and records candidate feature-set hashes. - During
ledgr_run()orledgr_sweep(), the engine computes the registered feature values at each pulse without looking past the current bar. - Strategy code normally reads one feature across the decision axis with
ctx$vec$feature(). Scalar accessors remain useful for inspection and for active aliases that have no whole-universe alias accessor yet.
A fingerprint identifies the feature definition, not just the name. If the calculation, parameters, warmup rule, adapter, or selected output changes, the fingerprint changes even when the feature ID stays readable. Feature IDs identify values inside the pulse context. Fingerprints identify the feature definition used to compute those values. For multi-output sources such as TTR BBands or MACD, each selected output is an ordinary indicator with its own feature ID and output-specific fingerprint.
Feature IDs in one map must be unique. An alias does not make two different definitions with the same engine ID safe, and an alias may not equal another entry’s feature ID. It may equal its own feature ID. These fail-closed rules keep scalar engine-ID reads and mapped alias reads from naming different series. An alias never changes the underlying feature ID or fingerprint.
A bundle is an authoring convenience for declaring several indicator outputs at once. The engine receives ordinary single-output feature definitions after the bundle is flattened. The multi-output bundle helper follows the same lifecycle. It is not a second feature system.
The same idea works for crossover rules. An SMA crossover registers two separate indicators: one short moving average and one long moving average. The economic meaning is “fast trend above slow trend” rather than “close above one trend line.” Each moving average has its own feature ID, warmup, and stored values.
crossover_features <- ledgr_feature_map(
sma_fast = ledgr_ind_sma(10),
sma_slow = ledgr_ind_sma(30)
)
crossover_ids <- ledgr_feature_id(crossover_features)
ledgr_feature_contracts(crossover_features)
#> # A tibble: 2 x 5
#> alias feature_id source requires_bars stable_after
#> <chr> <chr> <chr> <int> <int>
#> 1 sma_fast sma_10 ledgr 10 10
#> 2 sma_slow sma_30 ledgr 30 30In a strategy, the crossover condition is just a comparison of the two mapped aliases after warmup:
Inspect One Pulse
Create a small sealed snapshot and inspect one decision pulse before running a full backtest. ledgr_pulse_snapshot() builds the ctx a strategy would receive at one timestamp; vignette("strategy-authoring-tools", package = "ledgr") uses it to test strategies. This keeps the runtime data visible before the article introduces more metadata.
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")
)
)
snapshot <- ledgr_snapshot_from_df(
bars,
snapshot_id = "indicators_demo"
)
pulse <- ledgr_pulse_snapshot(
snapshot,
universe = c("DEMO_01", "DEMO_02"),
ts_utc = ledgr_utc("2019-03-01"),
features = features
)At this timestamp, ledgr has computed the two features for each instrument in the universe through the same feature path a dense run uses over snapshot history through this timestamp. That includes each declaration’s parameters, stable_after, and, for a custom indicator computed over the whole series, its series_fn (vignette("custom-indicators", package = "ledgr")). The long pulse view shows the result directly: one row per instrument and feature. Without a feature map, alias is NA. With the map, rows are filtered to the mapped features and aliases are filled.
ledgr_pulse_features(pulse, features)
#> # A tibble: 4 x 5
#> ts_utc instrument_id feature_id feature_value alias
#> <dttm> <chr> <chr> <dbl> <chr>
#> 1 2019-03-01 00:00:00 DEMO_01 return_5 0.0853 ret_5
#> 2 2019-03-01 00:00:00 DEMO_01 sma_10 99.8 sma_10
#> 3 2019-03-01 00:00:00 DEMO_02 return_5 0.00402 ret_5
#> 4 2019-03-01 00:00:00 DEMO_02 sma_10 68.2 sma_10The wide pulse view is useful for debugging and ML-style row-per-observation workflows. It contains one OHLCV block and one feature block for each instrument. OHLCV columns use {instrument_id}__ohlcv_{field}. Feature columns use {instrument_id}__feature_{feature_id}. A feature map filters and orders feature columns, using aliases as the wide feature keys.
ledgr_pulse_wide(pulse, features)
#> # A tibble: 1 x 17
#> ts_utc cash equity DEMO_01__ohlcv_open DEMO_01__ohlcv_high
#> <dttm> <dbl> <dbl> <dbl> <dbl>
#> 1 2019-03-01 00:00:00 100000 100000 103. 107.
#> # i 12 more variables: DEMO_01__ohlcv_low <dbl>, DEMO_01__ohlcv_close <dbl>,
#> # DEMO_01__ohlcv_volume <dbl>, DEMO_01__feature_ret_5 <dbl>,
#> # DEMO_01__feature_sma_10 <dbl>, DEMO_02__ohlcv_open <dbl>, DEMO_02__ohlcv_high <dbl>,
#> # DEMO_02__ohlcv_low <dbl>, DEMO_02__ohlcv_close <dbl>, DEMO_02__ohlcv_volume <dbl>,
#> # DEMO_02__feature_ret_5 <dbl>, DEMO_02__feature_sma_10 <dbl>ledgr_pulse_features() and ledgr_pulse_wide() work on interactive pulse snapshots and on the ctx object inside an ordinary strategy function. They are inspection views over the same pulse-known data used by ctx$feature() and ctx$features().
Access Features In A Strategy
The long and wide pulse views are useful when you want to inspect the computed data, compare instruments, or think in model-like rows. They are not always the clearest shape for strategy code. A strategy often wants to ask a smaller question: “what are the current values for this instrument?”
That is why ledgr also exposes the same pulse data through scalar and mapped accessors. The table views and the accessors are not competing APIs; they are different views over the same pulse-known data.
The explicit scalar accessor is useful when you want to show or debug one value. It uses the engine ID, not the alias:
ids <- ledgr_feature_id(features)
pulse$feature("DEMO_01", ids[["ret_5"]])
#> [1] 0.08531877Mapped access returns a named numeric vector keyed by alias for one instrument at one pulse:
x <- pulse$features("DEMO_01", features)
x
#> ret_5 sma_10
#> 0.08531877 99.79637070
ledgr_passed_warmup(x)
#> [1] TRUEWarmup is the period before a known feature has enough prior bars to produce a usable value. A warmup NA is not an unknown feature; it is a known feature that is not ready yet.
Try it
Change the scalar accessor to pulse$feature("DEMO_01", "returns_5"). Why does that fail while ids[["ret_5"]] works?
A strategy decides for every instrument at once. ctx$vec$feature() reads one feature for the whole universe in a single call, one value per instrument in the order of ctx$universe. It takes the engine ID, which is what ids already holds:
That pattern keeps the signal logic readable:
-
featuresis where feature identity and aliases live;idsturns an alias into the engine ID. -
readyis the warmup gate: an instrument qualifies only once both of its features are usable. For one instrument,ledgr_passed_warmup()asks the same question of the vectorctx$features()returns. - The rest of the condition is the economic rule.
- The strategy still returns ordinary target quantities.
The rule reads the feature vectors by hand because reading them is this section’s lesson. ledgr_signal_feature(ctx, ids[["ret_5"]]) reads the same vector for the helper pipeline that vignette("strategy-development", package = "ledgr") shows.
ctx$features() in a loop over ctx$universe would give the same targets, but it repeats per-instrument work at every pulse. Keep the scalar and mapped accessors for inspecting one instrument.
The strategy reads ids from the code around it, so it closes over a value built from the feature map. The run records the registered feature definitions, but the stored strategy source refers to ids only by name. Keep the code that builds features and ids with your research record; vignette("reproducibility", package = "ledgr") explains why such a strategy is Tier 2.
Run The Example
The experiment registers the indicator objects. ledgr computes those features for every instrument at each pulse, then gives the strategy only the pulse-time values.
exp <- ledgr_experiment(
snapshot = snapshot,
strategy = strategy,
features = features,
opening = ledgr_opening(cash = 10000),
cost_model = ledgr_cost_zero()
)
bt <- exp |>
ledgr_run(
params = list(min_return = 0, qty = 10),
run_id = "indicators_demo_run"
)
#> Warning: LEDGR_LAST_BAR_NO_FILL: target changed on the final available bar, but the
#> next-open fill model requires a following bar. No fill was emitted for this target
#> change. Check the strategy's final-pulse behavior or extend the snapshot if this trade
#> should be fillable.
ledgr_results(bt, what = "fills")
#> # A tibble: 39 x 10
#> event_seq ts_utc recording_pulse_ts_utc instrument_id side qty price fee
#> <int> <date> <dttm> <chr> <chr> <dbl> <dbl> <dbl>
#> 1 1 2019-01-23 2019-01-23 00:00:00 DEMO_01 BUY 10 88.0 0
#> 2 2 2019-01-30 2019-01-30 00:00:00 DEMO_02 BUY 10 71.1 0
#> 3 3 2019-02-01 2019-02-01 00:00:00 DEMO_02 SELL 10 69.3 0
#> 4 4 2019-02-06 2019-02-06 00:00:00 DEMO_01 SELL 10 92.9 0
#> 5 5 2019-02-13 2019-02-13 00:00:00 DEMO_01 BUY 10 93.9 0
#> 6 6 2019-02-19 2019-02-19 00:00:00 DEMO_02 BUY 10 68.7 0
#> 7 7 2019-02-25 2019-02-25 00:00:00 DEMO_02 SELL 10 67.5 0
#> 8 8 2019-03-08 2019-03-08 00:00:00 DEMO_02 BUY 10 68.9 0
#> 9 9 2019-03-11 2019-03-11 00:00:00 DEMO_01 SELL 10 106. 0
#> 10 10 2019-03-11 2019-03-11 00:00:00 DEMO_02 SELL 10 68.0 0
#> # i 29 more rows
#> # i 2 more variables: realized_pnl <dbl>, action <chr>
close(pulse)
close(bt)The LEDGR_LAST_BAR_NO_FILL warning comes from the last bar: the rule changed its target there, and there is no later open at which to fill it. It is expected at the end of most samples; vignette("execution-semantics", package = "ledgr") explains it.
The fill table is the first execution check: it shows which instruments actually crossed from a target into a position, at which next-open price. An empty table would be a prompt to inspect warmup and signal values before trying more parameters.
Read The Feature Contracts
After you have seen the feature values at a pulse, the contract table is easier to read. The feature contracts are what ledgr will compute for every instrument in the run. alias is for your strategy code. feature_id is the stable engine ID. Warmup metadata tells you when a known feature may still be NA.
ledgr_feature_contracts(features)
#> # A tibble: 2 x 5
#> alias feature_id source requires_bars stable_after
#> <chr> <chr> <chr> <int> <int>
#> 1 ret_5 return_5 ledgr 6 6
#> 2 sma_10 sma_10 ledgr 10 10Plain lists remain valid too. For a named list, names become aliases in the contract table. For an unnamed list, alias is NA.
plain_features <- list(ledgr_ind_returns(5), ledgr_ind_sma(10))
ledgr_feature_contracts(plain_features)
#> # A tibble: 2 x 5
#> alias feature_id source requires_bars stable_after
#> <chr> <chr> <chr> <int> <int>
#> 1 <NA> return_5 ledgr 6 6
#> 2 <NA> sma_10 ledgr 10 10Parameter Grids Register Every Needed Feature
If a parameter grid changes a lookback, register every lookback variant before the run. ledgr does not create indicators dynamically from params; the run only computes the feature contracts registered on the experiment.
swept_features <- ledgr_feature_map(
ret_5 = ledgr_ind_returns(5),
ret_10 = ledgr_ind_returns(10),
ret_20 = ledgr_ind_returns(20)
)
ledgr_feature_id(swept_features)
#> ret_5 ret_10 ret_20
#> "return_5" "return_10" "return_20"The feature set must cover the whole grid: lookback = 20 means return_20 must already be registered. A missing feature ID is an unknown-feature error, not warmup. In short, all feature parameter values must be registered before ledgr_run(); do not create ledgr_ind_returns(params$lookback) lazily inside the strategy.
For exploratory sweeps over ledgr-owned indicator parameters, prefer active aliases. Declare the varying constructor argument with ledgr_param(); each sweep candidate then resolves its own concrete indicator behind a stable alias:
active_features <- ledgr_feature_map(
fast = ledgr_ind_sma(ledgr_param("fast_n")),
slow = ledgr_ind_sma(ledgr_param("slow_n"))
)
ledgr_snapshot_close(snapshot)vignette("sweeps", package = "ledgr") composes the feature and strategy grids for this map, runs the sweep and reads the candidate rows.
For single-output indicators, the feature-map alias is the strategy-facing name. Read it once across the decision axis with ctx$vec$feature("alias"); use ctx$features(id) when you deliberately need several mapped values for one instrument. On an axis of at least 100 instruments, an active-map bundle loop warns when its calls reach the axis size and names the plane replacement. Explicit-map bundle calls remain local and unrecorded. There is no plural whole-universe accessor: read each required alias through the singular plane. Bundle entries are intentionally flat; the TTR companion explains why bundles use prefix or naming rather than one outer alias.
For TTR-backed declarations, multi-output bundles, and adapter warmup rules, read vignette("ttr-indicators", package = "ledgr").
Troubleshoot Warmup And Zero Trades
Warmup problems are easiest to diagnose by connecting four facts:
-
ledgr_feature_contracts(features)tells you how many bars each feature needs before it can produce a usable value. -
ledgr_feature_contract_check(snapshot, features)joins those contracts to the actual per-instrument bar counts in the snapshot. -
ledgr_pulse_features(pulse, features)shows the current pulse-known values for the instruments and aliases you registered. -
summary(bt)printsWarmup Diagnosticswhen a completed run has registered features that can never become usable for an instrument because available bars are below the feature contract.
warmup_check_snapshot <- ledgr_snapshot_from_df(
bars |>
group_by(instrument_id) |>
filter(instrument_id != "DEMO_02" | row_number() <= 4L) |>
ungroup(),
snapshot_id = "indicators_warmup_check"
)
ledgr_feature_contract_check(warmup_check_snapshot, features) |>
select(alias, instrument_id, available_bars, stable_after, warmup_achievable)
#> # A tibble: 4 x 5
#> alias instrument_id available_bars stable_after warmup_achievable
#> <chr> <chr> <int> <int> <lgl>
#> 1 ret_5 DEMO_01 129 6 TRUE
#> 2 sma_10 DEMO_01 129 10 TRUE
#> 3 ret_5 DEMO_02 4 6 FALSE
#> 4 sma_10 DEMO_02 4 10 FALSE
ledgr_snapshot_close(warmup_check_snapshot)The warmup_achievable column is FALSE when an instrument does not have enough available bars to satisfy a feature’s stable_after contract.
Normal early warmup is temporary: a feature is NA near the beginning of an instrument’s sample and later becomes finite. Impossible warmup is different: the instrument never has enough available bars for that feature. In that case, zero trades can be a valid completed run plus a useful diagnostic, not a failed run.
Diagnose A Successful Run With Zero Trades
A completed run with zero trades is not automatically wrong. It means ledgr accepted the strategy outputs and the ledger reached the end of the sample, but no fill closed any quantity.
Use this checklist before changing the strategy:
- Start with
summary(bt). IfClosed Tradesis zero,win_rateandavg_tradeshould beNA, not zero. If a registered feature can never become usable because the sample is too short,summary(bt)also prints aWarmup Diagnosticsnote with the feature ID, instrument ID, required bars, and available bars. - Inspect
ledgr_results(bt, what = "fills"). Empty fills mean nothing ever executed. Non-empty fills with empty trades mean positions opened but did not close. - Confirm the feature IDs with
ledgr_feature_id(features). A helper such asledgr_signal_return(ctx, lookback = 60)readsreturn_60; that indicator must be registered before the run. - Compare feature contracts with sample length before assuming the strategy is broken.
ledgr_feature_contracts(features)showsrequires_barsandstable_after. If an instrument has fewer available bars than a feature’srequires_bars, that feature cannot become usable for that instrument. - Inspect a late pulse with
ledgr_pulse_snapshot(). If the feature is stillNAfor every instrument near the end of the sample, the issue is no longer ordinary early warmup. Check the lookback length, sample length, universe, and feature registration. - If the helper pipeline returns a
ledgr_empty_selectionon a late diagnostic pulse, inspect the signal values directly. Ordinary early warmup should have passed by then; a late all-missing signal usually points to sample length, universe, or feature-registration issues.
Timestamp checks should compare normalized UTC values, not local string representations. The chunk is a fragment for use inside a strategy or on a pulse you have built, so it is not executed here:
If fills are empty, distinguish zero signals from zero sizing. In your own run, use the same snapshot, universe, timestamp, features, strategy, and params you are debugging. The ... placeholders stand for those values, so this fragment is not executed here:
pulse <- ledgr_pulse_snapshot(
snapshot,
universe = ...,
ts_utc = ...,
features = features
)
target <- strategy(pulse, params)
target
all(target == 0)Custom fill-model contract errors are different from zero-trade outcomes; see the fill-model reference documentation for the required fill fields.
Three Warmup-Adjacent Cases
Warmup is per instrument. One instrument can have a usable value while another is still NA because it has fewer bars or a different data history.
Ordinary feature warmup is local to the beginning of each instrument’s usable sample. A known feature is NA for early pulses, then becomes usable once the feature contract has enough bars.
Impossible warmup is different: every value for an instrument/feature remains NA because the instrument never reaches the feature contract. That is the case reported by the Warmup Diagnostics note in summary(bt).
Current-bar absence is a third failure mode. If ledgr cannot construct the pulse sequence because a current bar is absent for an instrument in the requested universe, the run fails before strategy evaluation for that incomplete pulse; this is a pulse construction error, not a feature warmup value.
Expected Sessions And Certified Gap Behavior
The dense lifecycle above is the entrance. Availability-aware research adds a stricter question: which sessions should have contained an observation? A missing observation on a declared open session is a gap. By default the experiment’s history policy carries each price field for up to five expected sessions; ledgr_history_policy(price_carry = NULL) keeps the strict rule, under which the gap invalidates every bounded window that contains it. Return- and volatility-family indicators never use a carried price, and ledgr never calls a gap ordinary startup warmup.
strict_smas <- ledgr_feature_map(
built_in = ledgr_ind_sma(2),
public_ttr = ledgr_ind_ttr("SMA", input = "close", n = 2),
custom = ledgr_indicator(
"custom_sma_2",
function(window) mean(window$close),
requires_bars = 2,
gap_contract = "strict_window"
)
)
ledgr_feature_contracts(strict_smas)
#> # A tibble: 3 x 5
#> alias feature_id source requires_bars stable_after
#> <chr> <chr> <chr> <int> <int>
#> 1 built_in sma_2 ledgr 2 2
#> 2 public_ttr ttr_sma_2 TTR 2 2
#> 3 custom custom_sma_2 custom 2 2In the strict-gap table in vignette("missing-data-and-sessions", package = "ledgr"), which disables price carry, all three follow the same NA, 11, NA, NA, 15, 17 availability path. That is session equivalence, not a claim that different formulas are numerically identical.
| Form | Declaration | Availability-aware calculation |
|---|---|---|
| Built-in SMA, EMA, RSI or returns |
ledgr_ind_sma(n), ledgr_ind_ema(n), ledgr_ind_rsi(n), ledgr_ind_returns(n)
|
Supported: bounded |
| Registered TTR form |
ledgr_ind_ttr() on a catalogued function, output and argument form |
Supported: per catalogue row |
| TTR output bundle | ledgr_ind_ttr_outputs(...) |
Supported: per catalogue row |
| Custom bounded declaration |
ledgr_indicator(..., history_class = "bounded") or gap_contract = "strict_window"
|
Supported: bounded |
| Undeclared custom or unregistered TTR form |
ledgr_indicator(...) without a history declaration, or an uncatalogued TTR form |
Supported: history-dependent |
A bounded indicator uses the last requires_bars expected sessions and is missing while that window is incomplete. A history-dependent indicator runs once per contiguous segment of available input and restarts, and warms up again, after a gap. Contradictory declarations refuse with ledgr_indicator_history_contradiction.
A delisting ends carry and windows: after each relisting, price carry never reaches back across the terminal interval, and a bounded indicator restarts, so it needs requires_bars sessions from the relisting before it is ready again and never averages prices from before the delisting with prices after it.
stable_after is the first-valid boundary: the first session after a start or a restart at which ledgr exposes a value. It is not a convergence promise. A history-dependent indicator that restarts after a gap is valid again once it passes stable_after, yet it can still differ from the value an uninterrupted series would have produced. In ledgr’s conformance measurements (TTR 0.24.4, one missing session at three positions), the restarted series needed 40 to 47 observations to come within a scaled 1e-6 of the uninterrupted series for TTR EMA with n = 10, 59 to 72 for the MACD outputs with nFast = 5, nSlow = 10 and nSig = 4, 101 to 109 for ATR with n = 10, and 157 to 190 for RSI with n = 14. Read those ranges as evidence about how long retained state takes to recover, not as a guarantee.
Before an availability-aware run, sweep or precompute uses its features, ledgr checks each definition for look-ahead at no more than four fixed positions on one instrument: the value recomputed from the history ending at a position must equal the prepared value, or preparation refuses with ledgr_feature_lookahead_detected. The check is a sample, so passing it does not prove that a definition is causal, and dense experiments do not run it. vignette("custom-indicators", package = "ledgr") describes the extra check for custom definitions that supply both fn and series_fn.
For result-table interpretation after a zero-trade run, read vignette("metrics-and-accounting", package = "ledgr").
Where Next
-
vignette("leakage", package = "ledgr")shows how a feature can leak future information and which boundaries ledgr enforces. -
vignette("ttr-indicators", package = "ledgr")covers TTR-backed indicators and multi-output bundles. -
vignette("custom-indicators", package = "ledgr")covers writing your own indicators. -
vignette("strategy-development", package = "ledgr")uses feature values inside complete strategies.