DECCF · Cash Flow Modeler and Calculator

Engine concepts

This guide covers model declarations, calculation behavior, and result data.

1. Model lifecycle

A model moves through three steps:

  1. Create typed references for inputs, entities, accounts, schedules, and behaviors.
  2. Collect them in defineModel(). run() compiles and validates the declarations automatically; call compileModel() directly when validation should happen earlier.
  3. Evaluate the model with run(), compare runs with runScenarios(), or sample uncertain inputs with a seeded runMonteCarlo() call.

References are immutable objects with internal identity. Calculation functions read them with ctx.get(ref) and other explicit context methods. Reading a reference that is not declared in the model raises a runtime error. JavaScript can catch some mistakes, such as an undefined variable, as the model code is evaluated.

defineModel() accepts name, currency, timeline, and optional maps named preconditions, entities, accounts, phases, flows, agreements, events, options, components, and metrics. Map keys become names in the result. Calculation dependencies use reference objects rather than string lookups.

Declarations and published series use deterministic name ordering. dependsOn orders cashflows topologically; otherwise independent cashflows follow their declaration order.

2. Currency and amounts

currencies contains ready references for USD, SEK, EUR, GBP, NOK, DKK, CHF, CAD, AUD, and JPY. currency("PLN") creates a reference for another three-letter uppercase code.

money(amount, currency) associates an amount with its denomination. scaleMoney(), addMoney(), and subtractMoney() preserve and check currency identity. convertMoney(value, target, rate) requires an explicit exchange rate in units of target currency per source currency. The engine does not fetch exchange rates.

A model declares a reporting currency. A caller can select another denomination for an individual run with run(model, { currency: currencies.EUR }); this keeps numeric amounts unchanged and does not perform FX conversion. Money with an explicit currency that differs from the model currency is rejected during compilation.

Amounts use JavaScript Number and IEEE 754 arithmetic. Currency checks do not provide decimal-exact arithmetic or minor-unit rounding. Apply rounding rules in model logic when needed.

3. Time, phases, and projection

periods.daily, weekly, monthly, quarterly, and annual create a timeline. from accepts YYYY-MM or YYYY-MM-DD; a month without a day begins on its first day. count is the number of result periods. projection adds calculation periods after those results.

Projection periods participate in calculation but do not appear in result.dates, published series, or built-in total, npv, irr, and wal metrics. Custom metrics run after the full projection and can read it with methods such as ctx.sum().

A phase is a named inclusive date range:

Phases must fit inside the model timeline and cannot overlap model periods. ctx.time.phase is the active phase name or null. ctx.inPhase(phaseRef) checks a phase by reference. schedule.phaseStart, phaseEnter, and phaseEnd create one-time occurrences at phase boundaries.

4. Inputs and preconditions

The input factory creates reusable preconditions:

  • decimal, rate, fraction, integer, date, currency, and money create scalar inputs.
  • str, id, and enum describe typed fields; object and objectArray group those fields into structured values and editable rows.
  • curve(points, { interpolation }) creates a dated series with step or linear interpolation.
  • derived(fn) creates a value from other declared references.
  • normal, logNormal, uniform, and triangular create distributions.

domain: [min, max] can constrain numeric inputs. Fractions must be between zero and one; integers must be whole; other numeric values must be finite. Invalid values are rejected rather than silently clipped. A distribution's clip range is an explicit separate rule.

A normal run() uses the distribution's central value: the mean for normal, expected value for log-normal, midpoint for uniform, and mode for triangular. ctx.draw(ref) takes one pseudorandom sample for that input during a run. ctx.sample(ref) takes a sample per owner and period. Without a Monte Carlo seed, both use their central value.

runMonteCarlo(model, { trials, seed }) requires an integer seed and returns summaries for numeric metrics. It evaluates stochastic trials and summarizes their sampled results; the seed initializes the pseudorandom draws but does not turn the simulation into a deterministic forecast. runScenarios() accepts named scenarios with overrides keyed by input references.

Inputs are named in defineModel().preconditions. Each run may override them through the inline JavaScript object deterministic.inputs. Each call gets fresh run state; input values do not leak between runs.

5. Entities, fields, and state

entity.asset, entity.party, entity.bundle, and entity.reference identify an entity family. parent creates a hierarchy. result.entity(ref) sums cashflows for the entity and its descendants.

A field stores a numeric value per period. initial sets its first value; next(previous, ctx) can calculate later values. Read a field with ctx.get(fieldRef) and its previous-period value with ctx.previous(fieldRef).

state("name") creates a state reference. lifecycle declares allowed states, an initial state, and optional transition guards. At most one transition per entity occurs in a period. result.state(entityRef) returns its state by period; result.transitions records state changes.

6. Cashflows and ordering

cashflow() creates a time series with a schedule, amount, and optional owner, direction, currency, agreement, category, and activation guard. Owners and directions are optional for posting flows. A numeric amount uses the model's currency. A calculation function runs only on scheduled periods when the flow is active.

Basic cashflows can use categories created with category(path). A category may also have a display label, for example category("sales.subscription", { label: "Subscription sales" }); labeled categories appear in deterministic.categories. There are no built-in result categories. A posting flow instead takes its category from each target account.

inflow and outflow are named values, not strings. Categories are independent of direction. A custom category path is a dot-separated lowercase identifier, and must be created with category().

ctx.get(flowRef) reads a completed current-period amount as Money. Same-period reads must appear in dependsOn; cycles are rejected. ctx.sum(flowRef, from, to) sums inclusive integer period indexes and cannot read future periods. ctx.previous(flowRef) reads the previous period's signed amount. Flows are evaluated after their declared dependencies.

Calculation functions should be synchronous and free of side effects. The engine recalculates them on each run and does not isolate JavaScript closures.

7. Accounts and posting flows

An account is a balance carried between periods. It can have an owner, a type (asset, liability, equity, income, expense, or transfer), a reporting category, and an opening balance. Posting flows use debit and credit or multiple bookings rows. Rows must balance. A unique asset among the booking targets is inferred as the cash account; set cashAccount explicitly when needed.

Debits increase an account's signed balance and credits decrease it. Income and expense report signs are adjusted so income is positive and expenses negative. Balance accounts follow the signed debit and credit movements. Each booking row inherits the category from its target account. A posting flow can omit owner; the engine infers it if all booked accounts share the same owner. It can omit direction when the cash account is known.

statement() is a flow-group declaration for absolute account targets. It reads the current account balances and posts only the difference. Recurring schedules distribute each remaining difference over the occurrences left in the schedule. The generated postings follow the same account categories and debit/credit rules as other posting flows.

result.category(ref) summarizes category movements; result.account(ref) returns ending account balances. The simpler moves: accountRef form remains available and uses that account's side (owed or due). See Post cashflows to accounts for examples.

8. Agreements, events, and options

agreementTerm(value) turns a value into a agreement term reference. A agreement attaches terms and named parties to a subject entity, with optional from, to, onStart, and onEnd actions. A cashflow that names a agreement runs within its agreement period. Use ctx.term(agreementRef, termRef) to read a agreement term.

An event uses a guard, a schedule, or both. Without a schedule, it fires when the guard changes from false to true. With a schedule, the guard is checked at each occurrence. actions.setState, setField, activate, and deactivate create typed actions.

An option has a guard and payoff function. By default it is exercised once when the guard becomes true at a scheduled check. maxExercises permits further exercises if the condition becomes false and true again.

9. Components, collections, allocation, and funding

component({ entities, flows, calculations, components, outputs }) groups ordinary declarations and names the values that callers may use. repeat(objectArray, template) instantiates one component per row with the row's typed projections. Use .where({ role: "adult" }) to select a view and .column("salary") to project keyed values. The same IDs connect people, salaries, tax, and payments across the model. See the declarative household guide.

allocation({ amount, method }) calculates shares using proportional({ weights }) or ordered priority({ order, requests }). It reports a calculation and does not post money. transfer({ from, to, amount, posting, schedule }) makes a balanced internal movement. funding({ from, to, amount, method, posting, schedule }) combines an allocation with ordinary transfers; optional limits and shortfall rules are shared with the same allocation solver. Funding outputs are calculations and do not add a second copy to cashflow totals.

10. Results and metrics

run(model, { discountRate }) performs an ordinary run. The optional annual effective discountRate defaults to zero and changes valuation, not cashflows. A custom metric callback runs at each declared frequency after its period is settled and can read its inclusive ctx.metricPeriod window. It returns a finite number, Money, or undefined; an undefined value leaves that period blank. Optional labels and units travel with the result series and are available to presentation layers.

Built-in result metrics include total, npv, irr, and wal. The result also exposes dates, series, transitions, events, account postings, and helpers such as flow(ref), option(ref), field(ref), account(ref), state(ref), entity(ref), and component(ref). Reference-based helpers verify that the reference belongs to the model.

The serializable result groups metrics under deterministic.metrics and time series under deterministic.series:

Each series has an index and values. Monetary values are objects containing amount and currency. Hash fields are not included in the serialized result.

11. Errors and random sampling

Inputs are checked when references are created, when a model is compiled, and during calculation. Domain errors use ModelError with a stable code and optional path, period, and details. JavaScript syntax errors and undefined variables retain their normal runtime behavior.

Monte Carlo treats distributions as uncertain inputs and samples them across trials. Its seed initializes the pseudorandom draws; its summaries describe those simulated outcomes and are not deterministic forecasts. The core does not read the clock, use the network, or rely on the global random source.