Skip to main content
CFDL

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's moic, or a declared metric
  • Per-entity and per-stream totals: entity.<name>.total, stream.<name>.total
  • Run facts: run.annual_discount_rate or run.annual_discount_curve, run.periods_per_year, and run.as_of when 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.json

Declared 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_cost

A 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>).