times <- as.POSIXct("2020-01-01 16:00:00", tz = "UTC") + 86400 * 0:3
bars <- data.frame(
instrument_id = "AAA",
ts_utc = times,
open = c(50, 48.75, 48.75, 48.75),
high = c(50, 48.75, 48.75, 48.75),
low = c(50, 48.75, 48.75, 48.75),
close = c(50, 48.75, 48.75, 48.75),
volume = 1000
)
terms <- data.frame(
fact_id = "example-dividend",
subtype = "ordinary_cash_dividend",
parent_instrument_id = "AAA",
entitlement_time = times[[2]],
effective_time = times[[2]],
knowledge_time = times[[1]],
complete = TRUE,
provenance_tier = "snapshot_bound",
gross_cash_per_parent_unit = 1.25,
gross_cash_validated = TRUE,
recipient_identity_validated = FALSE,
recipient_quantity_validated = FALSE,
source = "vignette-example"
)
store <- tempfile(fileext = ".duckdb")
snapshot <- ledgr_snapshot_from_df(
bars,
facts = ledgr_facts(ledgr_facts_equity_corporate_actions(terms)),
price_basis = "split_adjusted",
db_path = store,
snapshot_id = "cash_distribution_demo"
)
opening <- ledgr_opening(
cash = 1000,
positions = c(AAA = 2),
cost_basis = c(AAA = 50)
)
hold <- function(ctx, params) ctx$hold()Your strategy holds a stock that pays a dividend. Without corporate-action evidence the run cannot credit that cash; with it, you choose how and when it is credited. You need this article when distributions matter to your returns.
ledgr can post a fully evidenced ordinary cash distribution without enabling the broader availability model. The research preset uses the vendor-normalized gross amount and credits it at the ex-dividend session close.
That timing is a model assumption. It prevents a missing-asset dip on the ex-date, but makes cash spendable before a broker might actually pay it.
What The Print Lines Say
Every run print and summary ends with two lines about this evidence. Corporate actions: NOT SUPPLIED means the snapshot holds no corporate-action facts, so returns may omit distributions; MODELED means supplied evidence was applied under the settings the summary lists. Price basis: UNDECLARED means the bars do not say how they were adjusted: if they already include dividends and ledgr also credits them, the same distribution is counted twice. A declared basis such as split_adjusted removes that doubt. Both lines matter whenever the instruments pay distributions and the claim is about total return; for learning the mechanics on demo data they can be read and set aside.
A Minimal Evidenced Dividend
The bars are split-adjusted but not distribution-adjusted, so the price falls by the 1.25 distribution on the ex-date, the second bar, which is also the entitlement time. Two shares lose 2.5 of market value there, and the research preset posts the 2.5 of cash at that same close, so equity does not move: without the fact, the run would report the drop as a loss. In ledgr’s facts the entitlement time is that ex-dividend boundary, and it is the clock cash posts on: the research preset’s effective_close setting credits the cash at the close of the entitlement pulse. effective_time governs dispositions and quantity events, not cash. The cash term is already normalized to the sealed snapshot’s unit basis; ledgr does not reapply a vendor adjustment.
research <- ledgr_experiment(
snapshot,
hold,
opening = opening,
corporate_action_policy = ledgr_corporate_actions_research(),
cost_model = ledgr_cost_zero()
)
research_run <- ledgr_run(research, run_id = "dividend-research")
summary(research_run)
#> ledgr Backtest Summary
#> ======================
#>
#> Performance Metrics:
#> Total Return: 0.00%
#> Annualized Return: 0.00%
#> Max Drawdown: 0.00%
#>
#> Risk Metrics:
#> Risk-Free Rate: 0.00% annual
#> Annualization: 252 periods/year (US equity daily)
#> Volatility (annual): 0.00%
#> Sharpe Ratio: N/A
#>
#> Trade Statistics:
#> Closed Trades: 0
#> Win Rate: N/A (no trades)
#> Avg Trade: N/A (no trades)
#>
#> Exposure:
#> Time in Market: 100.00%
#>
#> Execution Evidence:
#> Fill Timing: dense_bar_timestamp
#>
#> Corporate-Action Evidence:
#> Corporate actions: MODELED - configured settlement conventions were exercised
#> Price basis: split_adjusted
#> Setting cash_amount: gross
#> Identity cash_amount: ledgr.corporate_action.cash_amount.gross.v001
#> Setting cash_posting: effective_close
#> Identity cash_posting: ledgr.corporate_action.cash_posting.effective_close.v001
#> Setting held_terminal_position: last_permissible
#> Identity held_terminal_position: ledgr.corporate_action.held_terminal_position.last_permissible.v001
#> Setting unsupported_quantity: report_only
#> Identity unsupported_quantity: ledgr.corporate_action.unsupported_quantity.report_only.v001
#> Exercised choices:
#> cash_amount.gross: 1
#> cash_amount.refuse: 0
#> cash_posting.effective_close: 1
#> cash_posting.next_open: 0
#> cash_posting.refuse: 0
#> held_terminal_position.last_permissible: 0
#> held_terminal_position.last_mark: 0
#> held_terminal_position.refuse: 0
#> unsupported_quantity.report_only: 0
#> unsupported_quantity.refuse: 0
#> Refusal reasons:
#> none declared: 0
#> Late arrivals: 0
#> Affected marked exposure: 97.5
#> Gross cash posted: 2.5
#> Modeled terminal proceeds: 0
#> Positions disposed: 0
#> Realized model P&L: 0
#> Unsupported facts: 0The annualized return and Sharpe ratio come from annualizing a few bars and mean nothing here; the total return and the corporate-action record are the result. In that record each setting appears twice, as its value and as the versioned identity that fixes what the value means, and the counts below it show which choices this run exercised. The modeled result reports one cash_amount.gross choice, one cash_posting.effective_close choice and gross cash posted of 2.5. It does not claim a payment date, withholding, investor tax or broker-net amount.
This release does not claim corporate-action completeness, broker-exact settlement, net cash, tax correctness, or exact recipient exposure.
Strict Refusal
The strict preset uses the same facts and refuses to model the cash effect.
strict <- ledgr_experiment(
snapshot,
hold,
opening = opening,
corporate_action_policy = ledgr_corporate_actions_strict(),
cost_model = ledgr_cost_zero()
)
strict_result <- tryCatch(
ledgr_run(strict, run_id = "dividend-strict"),
ledgr_corporate_action_unsupported = function(error) {
paste("Strict policy refused:", conditionMessage(error))
}
)
strict_result
#> [1] "Strict policy refused: Corporate-action cash settlement is refused by the selected policy."Where This Fits
This article keeps one local AAA holding so the gross cash amount and the ex-date price drop are easy to inspect. The full reusable point-in-time model is in vignette("point-in-time-inputs", package = "ledgr"); use it when the same study also needs sessions, membership, status, lifetime or quantity events. A dense study that only needs evidenced cash distributions does not have to adopt every fact family.
AAA is local to this article, not an alias for a shared DEMO_* instrument. When you want to model a dividend on data that also carries sessions, membership and trading status, load the shared ledgr_demo_pit_inputs bundle. It contains a cash dividend, and this check finds its terms:
data("ledgr_demo_pit_inputs", package = "ledgr")
shared_cash_case <- subset(
ledgr_demo_pit_inputs$cases,
type == "cash_dividend"
)
shared_cash_terms <- subset(
ledgr_demo_pit_inputs$corporate_actions,
parent_instrument_id == shared_cash_case$instrument_id[[1L]] &
as.Date(entitlement_time) == shared_cash_case$date[[1L]]
)
shared_cash_terms[
, c("subtype", "parent_instrument_id", "gross_cash_per_parent_unit")
]
#> subtype parent_instrument_id gross_cash_per_parent_unit
#> 1 ordinary_cash_dividend DEMO_03 0.75Where Next
- Read
vignette("survivorship-bias", package = "ledgr")for what replacing historical membership with today’s survivors does to a result. - Read
vignette("corporate-action-adapter-authoring", package = "ledgr")if you are translating vendor evidence into the canonical fact family. - Read
vignette("point-in-time-inputs", package = "ledgr")to place this fact inside the complete sealed data model. - Use
ledgr_corporate_action_summary()when you need the full policy and settlement report rather than the compact run print shown above.