Skip to main content
CFDLAcademy
All chapters

Part 5 · Reference · Chapter 26 of 28

Appendix A: language quick reference

Everything the course taught, compressed for the working desk. Shapes use angle brackets for what you supply and square brackets for what is optional; the normative grammar lives in the specification at cfdl.dev.

Model structure

version 0.1
model "<name>"
use pack "<id>" version "<ver>"        -- optional; root file only
import "<file>.cfdl"                   -- root and imported files; no cycles,
                                       -- never outside the model directory
time calendar <daily|monthly|quarterly|annual> from <date> for <N> [project <n>]
                                       -- project: a tail read by lookups only
phase <name> from <date> to <date>

One time grid per model; header statements live in the root file only. Everything else — entities, assumptions (a curve is one shape of assume), lifecycles, accounts, streams, contracts, events, options, waterfalls, metrics, slices, statements — may live in any file, in any order. Order never carries meaning (metrics excepted: they compose in declaration order).

Entities and fields

entity <family> <name> : <Type>                  -- family: asset | party |
entity <family> <name> : <Type> {                --   container | reference
  <field> = <literal | inputs.<name>>            -- a fact: a literal, or a
                                                 --   single-valued assumption
                                                 --   or a set's field
  <field> init <expr>                            -- a rule: the recurrence
          next <expr>                            --   `prev` = own prior value
  part of <family>.<name>                        -- hierarchy; rollups follow
  state <s>                                      -- opening state
  lifecycle <name>                               -- bind a machine
  account <name> <owed|due> init <expr>          -- an entity-owned claim
}

An entity may also carry an id (docs/01 §7.1). Reads, from any expression: asset.x.field (this period, at close), prev.asset.x.field (prior close), entity.field (the attached entity's own field). Rules see only the completed prior column — same-period cycles are unwritable.

Types form a refines chain ending at a master. The language base ships abstract contract masters (Contract.Debt, Contract.Lease, Contract.Option, …) that bind no lowering and cannot be instantiated — they exist so a slice's type Contract.Debt reaches every pack's debt transitively, and fields inherit down the chain.

Streams

stream <a>.<b>[.<c>] on entity <family>.<name> <inflow|outflow|accrual|writeoff>
       currency <CCY> [moves <account>] {
  [category <dotted.path>]                       -- bare, like `currency USD`;
  schedule <see below>                           --   rooted operating |
  [active when <boolean expr>                    --   investing | financing
   | active in state <name>]
  amount = <expr>
}

Names are dotted, at least two segments, unique. Direction is declared; amounts are written positive. accrual and writeoff are non-cash: an accrual moves a balance with no cash, a write-off retires one (docs/01 §9.1). moves <account> names the account the stream raises or lowers as it pays, and a later expression reads that balance as prev.<account>. category says what the cash is — a dotted path into the cash flow statement, first segment operating, investing, or financing. active in state gates on the owner's lifecycle, checked against the declared machine — where a string comparison against entity.status could be misspelled and false forever.

Schedules

schedule on <date> [start|mid|end] [convention <conv>] [calendar "<cal>"]
schedule every <day|week|month|quarter|year> [start|mid|end]
         [on day <n> | on eom]
         [net <n> [days|months]]
         from <date|phase_start("p")> to <date|phase_end("p")>
         [except [<dates>]] [also [<dates>]]
schedule on phase_enter("<phase>")

Placement: one axis, three positions, at most one — start (advance), mid (mid-period convention), end (arrears). Both every and on <date> take all three; only the default differs, end for a stride and start for a one-shot. Conventions: none, following, preceding, modified_following, modified_preceding. Calendars: "weekend", "us", "uk", "target". net counts days, or steps calendar months. Occurrences pushed off the grid are compile errors.

Assumptions and curves

assume <name> [: <type>] = <expr> ["<unit>"] [within [lo, hi]]
assume <name> [: <type>] ~ Normal(mean=, stdev=, clip=[lo, hi]) [within [lo, hi]]
             ~ LogNormal(mu=, sigma=)     ~ Uniform(min=, max=)
             ~ Triangular(min=, mode=, max=)
assume <name> [: <type>] = curve [step|linear] [from <date>] [to <date>] { <date>: <value>  ... }
assume <name> [: <type>] = quantile [step|linear] [by exceedance] { <share>: <value>  ... }
assume <name> : <SetType> = { <field> = <value>  <branch> = { ... } }   -- a set
assume <name> : <type>                         -- bodiless: the run supplies it
  ... source { publisher "<who>"  as_of <date> }                         -- provenance

Read as inputs.<name>, a set's field as inputs.<name>.<field>; a series at another date as curve_value(inputs.<name>, <date>). The type slot (rate, fraction, a set type) checks the value; within is the deal's own bound, checked wherever the value arrives and never clamped; a value may carry its unit, 60 "USD/sf"; source records where a number came from. Deterministic runs use central values: Normal → mean, LogNormal → exp(mu + sigma²/2), Uniform → midpoint, Triangular → (min + mode + max) / 3 — not the mode. A read outside a curve's stated range is refused (E5040); a curve with no to holds its last value forward, with a warning (W5024) (docs/01 §12.5).

A quantile is a curve turned sideways: indexed by cumulative share rather than by date, for a quantity whose dispersion drives the answer. Shares in 0..1, values non-decreasing; by exceedance writes the points worst-first, the way a duration curve reads, and is authoring surface only. Read with quantile_at, quantile_mean, quantile_of (expression table below). Never sampled: it is declared data.

Lifecycles and accounts

lifecycle <name> {                               -- a finite state machine
  initial <state>
  state <a>, <b>, <c>
  on enter <state> { set <field> = <expr> }      -- arrival action: the state
  <from> -> <to> [when <boolean expr>]           -- guard: checked each period
         [{ set <field> = <expr> }]              --   in <from>; re-entry re-arms;
}                                                --   an edge action: the path
account <name> [owed|due] {                      -- cash that accumulates
  [owner <family>.<name>]                        -- a party may own several
  [from <expr>]                                  -- per-period inflow; no floor
}

An entity binds a machine with lifecycle <name> in its block (or from its pack type, which a model may augment with its own states and edges). A contract may bind a machine too, and gate its lines with in <state> run <line>, <line>; a machine may nest within a state (docs/01 §7.3, §8.6). Edges are declared only as used; a guard-less edge is a permission an event's write may take. Declaration order resolves simultaneous guards; one transition per entity per period; a self-edge re-anchors. Logic reads series strictly backward — windows end at time.t - 1 — and reads a balance as prev.<account>. A window lying entirely before the first period makes a guard false; prev.<account> in the first period is the account's init, zero by default, never absent (docs/01 §7.3, §10.6). A waterfall draws an account with from <account> and a step credits one with pay <step> to account <name> = <expr>. A party that owns several accounts is paid by naming one; to party.<name> is then refused (E1324). The balance publishes as the non-cash series account.<name>. The third schedule anchor: every <interval> from state_enter(<ref>, <state>) for <n> periods — a window per entry, re-anchored on re-entry.

A stream has no window: an amount folding time.t + k is refused. A sale pays a valuation — amount = metric.cre.exit.gross_value — and is struck after the figure folds over the projection tail; logic reading what it pays is a refused cycle, with the path named.

Contracts, events, options, waterfalls

contract <pack>.<type> [<instance>] on entity <ref> {     -- or the fused
  term <date>..<date>                                     --   <pack>.<type>.<instance>
  term from state_enter(<ref>, <state>) for <n> periods   -- or opened by a state
  terms { <key> = <literal | inputs.name | expression>  ... }   -- a term may
                                                          --   name a set: inputs.<set>
  parties { <role> = party.<name>  ... }                  -- the type's roles
  payment <line> <start|mid|end|net ...>                  -- a line's placement
  lifecycle <name>                                        -- the contract's machine
  on start { set status = "<state>" }                     -- at its boundaries
  on end   { set status = "<state>" }
  effects { ... }                                         -- (docs/01 §8)
}

event <a>.<b> [schedule <as streams>] when <boolean expr> {   -- rising edge:
                                             --   fires on each occurrence, re-arms
                                             --   when the condition falls
  set entity <family>.<name>.<field> = <value>
  activate stream <name> | deactivate stream <name>
  exercise option <name>
}

option <a>.<b> [on entity <ref> | on contract <name>] type <Type>
       [exercisable [<n> times] [in <phase>]] {
  parties { <role> = party.<name>  ... }
  terms { <key> = <value>  ... }             -- read as contract.<key>
  schedule <as streams>                      -- optional; a Bermudan right
  exercise when <boolean expr>               -- the election; rising edge
  payoff <expr>
  set entity ... | activate stream ... | deactivate stream ...   -- on exercise
}

waterfall <a>.<b> on entity <ref> {
  schedule <as streams>
  from <expr | available | <account>>        -- the pot; an account's balance
  pay <step> to <family>.<name> = <expr>     -- receives min(max(0,owed),remaining)
  pay <step> to account <name> = <expr>      -- into an account
  pay <step> to ... for contract <c> line <l> = <expr>   -- a contract's line
  pay for contract "<selector>" line <l>     -- the steps the contracts contribute
}

Waterfall step expressions may read remaining, owed.<step>, paid.<step> (earlier steps only). The five step shapes: stated amount, capped (min), pay-to-target (balance − target), shortfall (owed.x − paid.x), sweep (remaining). Every step publishes shortfall.<waterfall>.<step>, owed less paid, which a later waterfall or a metric reads by name.

Metrics, slices, statements

The figures-and-views tier: constructs that read a finished projection and produce no cash.

metric <name> = <expr>                       -- one number at the horizon; folds
                                             --   published series; composes in
                                             --   declaration order
slice <name> {                               -- a named partial selection
  entity <family>.<name>                     -- plus its part-of descendants
  type <TypeId>                              -- matches through refines
  category "<path or path.*>"                -- quoted; series_sum's dialect
  stream "<selector>"
  line <role>                                -- a contract line, with type
  except stream "<selector>"                 -- kinds intersect; excepts subtract
  except entity <family>.<name>
  except category "<path>"
  window from <date> to <date>               -- bounds the PERIODS, not the
}                                            --   streams; at most one

statement <name> {                           -- generated: the tree renders
  label     "<title>"
  structure <entity|category>
  depth     <N>                              -- level of aggregation
  [grain <calendar>]                         -- re-buckets values; total stays
  [slice <name>]                             --   lifetime; ratios recompute
  [metrics  <a>, <b>]                        -- declared metrics, beside the rows
}
statement <name> {                           -- authored: the rows are stated
  label    "<title>"
  line     "<label>"  { category "<sel>" | stream "<sel>" | slice <name>
                        | type <TypeId> [line <role>]
                        | entity <family>.<name> [display positive] [depth <N>] }
  subtotal "<label>"  { category "<sel>" }   -- folds rows stated elsewhere
  subtotal "<label>"  { series "<key>" }     -- presents a published fold; claims
                                             -- nothing, out of the bottom line;
                                             -- a claim beside it is E1370
  spacer
  ratio    "<label>"  { of <slice> to <slice> display positive }
}

A metric is evaluated once, at the horizon, over the finished projection. It may fold any series the model publishes — a stream by either spelling (ops.rev or stream.ops.rev), a waterfall step, entity.<sym>.net_cash_flow, account.<name>, an entity field, a money subtotal, model.net_cash_flow — and folding an unpublished name is refused at compile (E1365), not read as zero. irr(party.x) and moic(party.x) are legal only in a metric: folds over the party's own account, refused elsewhere (E1355).

A statement is authored or generated, never both, and never neither. slice filters orthogonally to the structure, and a sliced statement reconciles against the slice's total. A slice and a statement are views — they change no value and move neither hash; a metric is a figure the model claims, and does move the model hash. A model that declares no statement gets a default one: the entity hierarchy, marked default in results.

Expressions

Scopes: time.t (period index from 0), time.date, time.phase, time.ppy (periods per year), time.days_in_period · inputs.* (the model's assumptions, and what the run supplies into a bodiless one) · contract.<term> (in an option or a contract's own blocks) · reference.<name>.<field> · slice.<name> · field reads as above. Arithmetic is decimal; % is remainder, not percent.

FamilyFunctions
Bounds and shapemin max abs clamp(x, lo, hi) sum avg
Growthpow exp ln
Roundinground(x, digits) round_up round_down · round_to(x, step) — nearest multiple
Conditionalif(cond, then, else)
Datesdate parse_date edate eomonth days_between months_between year_frac is_business_day add_business_days roll
TVMpv fv pmt(rate, nper, pv) ipmt ppmt nper rate
Seriesseries_sum series_avg series_min series_max series_prod series_count, each ("name", from_t, to_t) — dependency-ordered; chains to any depth, cycles refused
Quantilesquantile_at(inputs.name, share) quantile_mean(inputs.name, from, to) — exact partial expectation · quantile_of(inputs.name, value) — the inverse: threshold → share
Domaincpr_to_smm cpr_to_periodic(x, ppy) macrs_rate(year, life) curve_value wal
Structure and statepart_sum(<ref>, <field>[, <filter>]) part_count state_enter(<ref>, <state>) — the period of the latest entry
Drawssample(inputs.name) — a draw for the reader's entity and period
Returns and valuations (metrics only)irr(party.x) moic(party.x) · npv("selection", rate[, from_t]) — present value of what the selection pays · yield("selection", outlay[, from_t]) — the annual rate at which that value equals the price paid · spread("selection", outlay, inputs.index[, from]) — the spread over an index path at which it equals the price paid, a floater's discount margin; from a period or a date, the outlay's; each null when nothing solves

Every series reduction folds the per-period aggregate: streams a selector matches are added within each period first, then the fold runs over that one series. An empty selection returns each fold's identity — sum 0, product 1, count 0 — and series_max/series_min, which have none, return null. series_avg divides by the requested window less the undefined cells in it — the periods it actually folded, which for a cash series is the whole requested window; series_count counts the periods whose aggregate is non-zero, and if(series_count("x.*", 0, t) == 0, 0, series_max("x.*", 0, t)) is how a legitimately-empty selector says so.

Full signatures: the expressions reference at cfdl.dev.

Run configuration

{
  "deterministic": { "annual_discount_rate": 0.08, "valuation_grain": "annual",
                     "as_of": "2026-01-01", "arithmetic": "excel_compat",
                     "parameters": { "inputs.z": 42 }, "inputs": "inputs.json" },
  "scenarios": { "<name>": { "annual_discount_curve": "<assumption>",
                             "parameters": {}, "inputs": "<scenario inputs>.json" } },
  "monte_carlo": { "trial_count": 1000, "seed": 42,
                   "distributions": { "inputs.g": { "kind": "uniform", "min": 0.01, "max": 0.05 } } }
}

Annual rates convert to the grid geometrically: (1 + r)^(1/n) − 1; annual_discount_curve names a series-shaped assumption instead. Parameters override inputs.<name> per scenario without touching the model; each scenario may name its own inputs file. arithmetic is decimal unless set to "excel_compat", for reconciling against a workbook (docs/09 §11). A run monte_carlo trials N seed N statement in the model is overridden by the configuration when both exist.

The disciplines, one line each

Model at the grain cash moves; roll up, never slice down. Opinions on the assumption page; structure literal. Dates in phases. Expressions that read aloud. One hand-checked anchor per file. Match a source's method, then its answer. Recompute ratios from rolled-up flows. Fix the first error only. Predict before you run.