type instrument_id date
1 venue_closure <NA> 2020-01-06
2 missing_observation DEMO_04 2020-01-09
3 delisting DEMO_01 2020-01-14
4 halt DEMO_02 2020-01-03
5 cash_dividend DEMO_03 2020-01-10
Bars tell you what was observed. They do not tell you whether a venue was open, which instruments belonged to a research universe, whether an instrument could trade, or when evidence became knowable.
This article adds those claims one at a time. By the end, you will have sealed and run one point-in-time bundle, seen a late-known halt change the public decision view, and have a reference map for every accepted input family.
You need this section when your research depends on calendars, a universe that changes over time, trading restrictions, or corporate actions. A study on complete daily bars for a fixed set of instruments can skip it and continue with vignette("strategy-authoring-tools", package = "ledgr").
An experiment becomes availability-aware when its snapshot declares membership, session, trading-status or lifetime facts, or when the experiment declares a valuation policy. There is no separate switch. Corporate-action facts alone do not turn it on; they change how held positions are valued and credited, not which instruments the strategy sees.
The bundle is synthetic and self-contained. Its bars, physical instrument master, calendar, and facts describe the same five DEMO_* instruments over the same window. Do not attach its facts to an unrelated observation panel.
Start With The Missing Observation
The manifest says DEMO_04 has no bar on an otherwise open date.
# A tibble: 1 x 3
instrument_id session_date observation_present
<chr> <date> <lgl>
1 DEMO_04 2020-01-09 FALSE
A price file alone cannot distinguish this gap from a venue closure. The independent session calendar makes that distinction.
session_date status session_close
1 2020-01-06 closed <NA>
2 2020-01-09 open 16:00:00
The closure is declared closed; the gap date remains open. ledgr can then advance the expected-session clock without manufacturing a price. Declaring any availability family requires a complete session calendar. Under knowledge = "evidenced", every session row needs a knowledge_time: an open session must be known by its open, and a closure before its civil day starts. Use "assume_effective" only when that assumption is part of the research design. vignette("missing-data-and-sessions", package = "ledgr") is the home for session calendars, trading status and stale marks; this article only assembles them.
Add A Point-In-Time Universe
Universe membership answers a different question: which physical instruments were eligible for this research population? This bundle declares one complete membership set.
membership_facts <- ledgr_facts_membership_snapshots(
pit$membership,
universe_id = "demo_members",
complete = TRUE,
knowledge = "evidenced"
)
pit$membership |>
select(effective_from, knowledge_time, instrument_id) effective_from knowledge_time instrument_id
1 2020-01-01 21:00:00 2019-12-31 05:00:00 DEMO_01
2 2020-01-01 21:00:00 2019-12-31 05:00:00 DEMO_02
3 2020-01-01 21:00:00 2019-12-31 05:00:00 DEMO_03
4 2020-01-01 21:00:00 2019-12-31 05:00:00 DEMO_04
5 2020-01-01 21:00:00 2019-12-31 05:00:00 DEMO_05
Because the set is complete, omission means non-membership for that effective set. Use interval assertions instead when your source supplies individual membership changes rather than complete lists.
For membership under knowledge = "evidenced", a missing knowledge time is kept for audit but cannot affect resolution. Membership evidence may become knowable after it took effect; "assume_effective" deliberately removes that distinction.
This bundle keeps all five instruments in the set. To see membership changes alter a result, continue with vignette("survivorship-bias", package = "ledgr").
Effective Time Is Not Knowledge Time
DEMO_02 was halted from 3 January, but the bundle says that evidence became knowable only on 7 January. The fact must not change decisions made earlier.
instrument_id effective_from effective_to knowledge_time status
1 DEMO_02 2020-01-01 21:00:00 <NA> 2019-12-31 05:00:00 active
2 DEMO_02 2020-01-03 21:00:00 2020-01-08 21:00:00 2020-01-07 21:00:00 halted
precedence
1 0
2 1
The open-ended active assertion has precedence zero. The overlapping halt has precedence one and wins only after it is known.
status_facts <- ledgr_facts_trading_status(
pit$trading_status,
knowledge = "evidenced"
)This separation is the core point-in-time rule: effective time says when the world changed; knowledge time says when the backtest may use the evidence. Status evidence may arrive late. A status row with missing evidenced knowledge stays audit-only rather than becoming an active restriction.
Add Lifetime And Economic Events When Needed
Lifetime facts distinguish an instrument that ceased to exist from one that merely lacks a bar. Corporate-action facts record evidenced economic events.
lifetime_facts <- ledgr_facts_lifetime(
pit$lifetime,
knowledge = "evidenced"
)
corporate_action_facts <- ledgr_facts_equity_corporate_actions(
pit$corporate_actions
)
inactive <- pit$lifetime |>
filter(assertion == "known_inactive")
inactive_id <- inactive$instrument_id[[1L]]
tibble(
instrument_id = inactive_id,
last_observation = max(
pit$bars$ts_utc[pit$bars$instrument_id == inactive_id]
),
known_inactive_from = inactive$effective_from[[1L]],
knowledge_time = inactive$knowledge_time[[1L]],
terminal_event = inactive$terminal_event[[1L]]
)# A tibble: 1 x 5
instrument_id last_observation known_inactive_from knowledge_time terminal_event
<chr> <dttm> <dttm> <dttm> <chr>
1 DEMO_01 2020-01-14 21:00:00 2020-01-14 21:00:00 2020-01-14 21:00:00 delisted
pit$corporate_actions |>
select(
subtype,
parent_instrument_id,
entitlement_time,
effective_time,
knowledge_time,
payment_time
) subtype parent_instrument_id entitlement_time effective_time
1 ordinary_cash_dividend DEMO_03 2020-01-10 21:00:00 2020-01-10 21:00:00
knowledge_time payment_time
1 2020-01-09 21:00:00 2020-01-13 21:00:00
Lifetime follows the same late-knowledge rule as status: a missing evidenced knowledge time leaves the row audit-only rather than changing the run.
Corporate actions have four clocks because entitlement, market effect, knowledge, and payment need not coincide. Do not collapse them into one vendor date during preparation. The flat run below holds no instrument, so it verifies the terms and clocks rather than a cash effect. The held-position consequence is demonstrated in vignette("corporate-action-cash", package = "ledgr"). Corporate-action facts alone do not activate the availability model.
Seal And Run The Combined Evidence
Combine the fact families, then pass the plain bundle through the ordinary snapshot API. The scope and knowledge choices are explicit in the code above; they are not hidden inside the bundle’s recipe. The store comes from ledgr_temp_store() because the code closes and reopens it; a real project passes a persistent path, so the sealed evidence outlives the R session.
pit_facts <- ledgr_facts(
session_facts,
membership_facts,
status_facts,
lifetime_facts,
corporate_action_facts
)
pit_path <- ledgr_temp_store(file.path(tempdir(), "ledgr_pit_demo.duckdb"))
pit_snapshot <- ledgr_snapshot_from_df(
pit$bars,
instruments_df = pit$instruments,
facts = pit_facts,
db_path = pit_path,
snapshot_id = "pit_demo_snapshot",
price_basis = "split_adjusted"
)
pit_snapshot_id <- pit_snapshot$snapshot_id
ledgr_snapshot_close(pit_snapshot)
pit_snapshot <- ledgr_snapshot_open(
pit_path,
snapshot_id = pit_snapshot_id,
verify = TRUE
)price_basis = "split_adjusted" declares how this synthetic panel was prepared; it does not adjust prices. Do not copy that declaration onto raw or fully adjusted vendor bars.
Availability-aware runs require an explicit stale-mark policy. The flat strategy below makes no trades; its purpose is to show what the decision view knows at the three halt boundaries. ledgr_valuation_stale(2) permits a held instrument’s last real close to value it for at most two declared sessions; it does not create an execution price.
flat_strategy <- function(ctx, params) ctx$flat()
pit_experiment <- ledgr_experiment(
pit_snapshot,
flat_strategy,
universe = ledgr_universe_members("demo_members"),
valuation_policy = ledgr_valuation_stale(2),
cost_model = ledgr_cost_zero()
)
pit_run <- ledgr_run(pit_experiment, run_id = "pit-demo-run")
halt_view <- bind_rows(
ledgr_run_explain(
pit_run,
halt$instrument_id[[1]],
halt$effective_from[[1]]
) |>
mutate(moment = "effective, not yet known"),
ledgr_run_explain(
pit_run,
halt$instrument_id[[1]],
halt$knowledge_time[[1]]
) |>
mutate(moment = "known"),
ledgr_run_explain(
pit_run,
halt$instrument_id[[1]],
halt$effective_to[[1]]
) |>
mutate(moment = "interval ended")
)
halt_view |>
select(moment, target_restricted, target_restriction_reason)# A tibble: 3 x 3
moment target_restricted target_restriction_reason
<chr> <lgl> <chr>
1 effective, not yet known FALSE ""
2 known TRUE "trading_halted"
3 interval ended FALSE ""
unlist(ledgr_run_completion(pit_run)[
c("completion_status", "complete_performance")
]) completion_status complete_performance
"DONE" "TRUE"
The first decision remains unrestricted because the halt was not yet known. The second is restricted for trading_halted. The excluded interval end is active again. ledgr_run_explain() reports what one decision knew about one instrument, and ledgr_run_completion() reports whether the run finished its horizon; the missing-data article teaches both readers.
Data Dictionary
Use this section as a reference after you understand why each family exists. Constructor help remains authoritative for exhaustive optional columns and accepted values.
| Input | Row grain and identity | Required core columns or shape | Scope and time | What it unlocks | Constructor help |
|---|---|---|---|---|---|
| Bars | One row per unique (instrument_id, ts_utc)
|
instrument_id, ts_utc, open, high, low, close; volume optional |
Physical instrument; observation clock | Prices, valuation and execution inputs | ledgr_snapshot_from_df() |
| Instruments | One row per unique instrument_id
|
instrument_id; descriptive fields optional |
Physical master; stable identity | Metadata and referential integrity | ledgr_snapshot_from_df() |
| Sessions | One row per civil date in a venue range |
session_date, status, knowledge_time (unless assume_effective); open rows also require session_open, session_close
|
venue_id; local calendar plus knowledge time |
Expected-session gaps and mark ageing | ledgr_facts_sessions() |
| Membership intervals | One assertion per instrument interval |
instrument_id, effective_from, member; optional effective_to
|
universe_id; half-open effective interval plus knowledge time |
Point-in-time eligibility changes | ledgr_facts_membership_intervals() |
| Complete membership snapshots | One set per effective/knowledge/source group |
effective_from plus row-wise instrument_id or one members list-column; complete is declared |
universe_id; omission means non-membership only when complete |
Complete point-in-time populations | ledgr_facts_membership_snapshots() |
| Trading status | One sourced assertion per interval and precedence |
instrument_id, effective_from, status, source; optional effective_to, precedence and supersession fields |
Physical instrument; effective and knowledge times | Hold/exit restrictions such as halts | ledgr_facts_trading_status() |
| Lifetime | One assertion per instrument interval |
instrument_id, effective_from, assertion; optional effective_to, terminal_event
|
Physical instrument; effective and knowledge times | Listing and terminal-state restrictions | ledgr_facts_lifetime() |
| Equity corporate actions | One identified economic fact |
subtype, parent_instrument_id, complete, provenance_tier; terms and refusal_reason are conditional |
Physical parent/recipient; entitlement, effective, knowledge and payment times | Supported settlement or explicit refusal | ledgr_facts_equity_corporate_actions() |
Every interval is half-open: effective_from is included and effective_to is excluded. Across families:
- identifiers stay stable for the history being sealed;
- observation and fact timestamps are whole-second UTC instants; and
- knowledge is declared per family rather than assumed to precede effect; for membership, status and lifetime, missing evidenced knowledge remains audit-only unless
assume_effectiveis declared. Sessions instead require a valid knowledge time under the evidenced policy.
Entity Relationships
The physical instrument master, venue calendar, and research universe are different scopes.
erDiagram
SNAPSHOT ||--o{ INSTRUMENT : contains
INSTRUMENT ||--o{ BAR : observed_as
SNAPSHOT ||--o{ SESSION : seals
SNAPSHOT ||--o{ MEMBERSHIP : seals
INSTRUMENT ||--o{ MEMBERSHIP : referenced_by
INSTRUMENT ||--o{ TRADING_STATUS : constrained_by
INSTRUMENT ||--o{ LIFETIME : described_by
INSTRUMENT ||--o{ CORPORATE_ACTION : parent_or_recipient
SNAPSHOT {
string snapshot_id PK
string snapshot_hash
}
INSTRUMENT {
string instrument_id PK
}
BAR {
string instrument_id PK,FK
datetime ts_utc PK
}
SESSION {
string venue_id PK
date session_date PK
}
MEMBERSHIP {
string universe_id
string instrument_id FK
datetime effective_from
datetime knowledge_time
}
TRADING_STATUS {
string instrument_id FK
datetime effective_from
datetime knowledge_time
}
LIFETIME {
string instrument_id FK
datetime effective_from
datetime knowledge_time
}
CORPORATE_ACTION {
string fact_id PK
string parent_instrument_id FK
string recipient_instrument_id FK
}
Corporate-action parent and optional recipient ids resolve against the physical master, not universe membership. Trading-status and lifetime facts also refer to physical instruments; neither makes an instrument a universe member.
What A Seal Establishes
After a successful seal, ledgr can rely on stable physical ids, whole-second bar timestamps, unique bar keys, fact referential integrity, validated family structure, and an immutable snapshot hash.
A seal does not imply a dense instrument-by-session rectangle, current membership, continuously fresh prices, inferred corporate-action terms, or knowledge before effectiveness. Those remain explicit evidence or policy questions.
Cleanup
close(pit_run)
ledgr_snapshot_close(pit_snapshot)Where Next
-
vignette("missing-data-and-sessions", package = "ledgr")follows a real price gap through valuation and execution. -
vignette("survivorship-bias", package = "ledgr")shows the consequence of replacing historical membership with today’s survivors. -
vignette("corporate-action-cash", package = "ledgr")shows a cash distribution changing portfolio accounting. -
vignette("data-input-and-snapshots", package = "ledgr")covers CSV and Yahoo imports, quarantine, and reopening. -
vignette("strategy-development", package = "ledgr")continues from sealed inputs to a strategy and its first run.