Metrics
A metric's prefix says who minted it: model.* is the engine's, domain.*
is the active pack's, and metric.* is the model's own — a figure declared
with metric <name> = <expr>.
Core metrics
model.total— every run.model.npv— only when the run states a discount rate (run.annual_discount_rate) or a discount series (run.annual_discount_curve). A run with neither publishes no NPV.model.irr— only where cash moved and the flows solve for a rate (solver and tolerance documented in the spec).model.payback_periods/model.payback_years,model.wal_years— only where cash moved.- No whole-model multiple: a multiple partitions cash by kind, so a model
states its own —
moic(party.<p>), a slice'smoic, or a declared metric - Per-entity and per-stream totals:
entity.<name>.total,stream.<name>.total - Run facts:
run.annual_discount_rateorrun.annual_discount_curve,run.periods_per_year, andrun.as_ofwhen one was stated
Domain metrics (per pack)
Packs declare metric sets computed when you run with --pack <name>
(CLI), pack= (Python), or pack (API): e.g.
domain.energy.tax_benefits, domain.credit.wal_years,
domain.credit.collections_multiple, CRE NOI/exit metrics, OpCo
EBITDA/exit proceeds. Each pack guide lists its set. Domain metrics publish
in the results' top-level domain_metrics section, each with its lineage,
not in deterministic.metrics.
cfdl run ir.json --packs packs --pack credit --out results.jsonDeclared metrics (per model)
A model may name the figure it solved for — a number neither the engine nor a pack mints:
metric gross_revenue = series_sum("ops.revenue", 0, 4)
metric total_cost = series_sum("ops.cost", 0, 4)
metric margin = metric.gross_revenue + metric.total_costA declared metric is evaluated once, at the horizon, over the finished
projection — a fold over completed results, never a recurrence that feeds
back into the walk. It may fold any series the model publishes — a stream by
name, a waterfall step, entity.<symbol>.net_cash_flow, account.<name>,
an entity field's series, a money subtotal, model.net_cash_flow — and read
inputs.*, the engine's model.* metrics, and metric.<name> for
any metric declared above it. Metrics compose in declaration order, the rule
waterfalls follow, so margin above reads the two before it; a forward or
circular reference is refused (E1354). Folding a name the model does not
publish is refused too (E1365), not read as zero.
A selector in a fold may be a pattern: prefix.* matches everything under
the prefix, a * segment matches exactly one segment, and | separates
alternatives ("fees.* | rent.*"). A metric may also read a declared slice
as slice.<name>, a waterfall step's shortfall as
shortfall.<waterfall>.<step>, and the weighted average life of what a
series paid as wal(<series>), which is null when nothing was paid.
Two folds price what a selection of series pays, each matched series at its
own placement in the period: npv("<selection>", rate[, from_t]) is the
present value at an annual rate of what the selection pays from the open of
from_t, and yield("<selection>", outlay[, from_t]) is the annual rate at
which that value equals the full price paid there. A price is an assumption,
so a scenario or a Monte Carlo run reaches it by name, and a bond-equivalent
yield is arithmetic on the result:
assume class_price = 0.1925
metric outlay = asset.io.original_balance * inputs.class_price
metric class_y = yield("g3.interest.io_interest", metric.outlay)
metric class_bey = 2 * ((1 + metric.class_y) ^ 0.5 - 1)A floater is quoted on its spread over the index rather than its yield, and
spread("<selection>", outlay, inputs.<index>[, from_t]) solves it: the
annual spread over the named index path at which what the selection pays is
worth the price paid, each period discounted at the index plus the spread on
the simple basis a floating coupon accrues on. A floater bought at par gives
back its own margin. The optional last argument of all three folds places
the outlay: a period, at its open, or a date such as
parse_date("2019-01-30"), a settlement date that may fall before the
model's first period. Every cell is measured from it on its settlement
date, a contract-attributed series on its contract's day count.
Two more folds exist in a metric and nowhere else — a participant's realized return, read from the party's own account:
metric lp_irr = irr(party.lp)
metric lp_moic = moic(party.lp)Outside a metric both are refused (E1355): a stream amount cannot ask for
a return on cash the stream has not produced yet.
Every declared metric is published as metric.<name> in
deterministic.metrics, in every scenario summary, and in every Monte Carlo
trial. A scenario or a trial that cannot evaluate a declared metric, such as
the IRR of a party that received nothing on that path, lists it under that
summary's omitted with the reason rather than publishing a number; the
deterministic run refuses instead.
Under Monte Carlo
Every metric — the engine's, the pack's, and the model's declared ones —
gets a distribution summary in the results' monte_carlo.metrics block:
mean, stdev, min/max, percentiles (p01–p99), and trials, the count of
trials that published that name.
In the Python SDK
results.metrics() returns a flat Series; results.metrics_frame() adds
currency and source lineage (core vs domain:<pack>).