Curves
Some inputs are not one number but a number per date: a forward power price, an interest-rate curve, a market-rent path, a decline profile. Chapter 6 gave scalars a home on the assumption page; a series is the same statement in another shape — assume <name> = curve { … } — and it completes the model's input vocabulary without leaving the page. The test for which you need is simple — if the true answer to "what is the assumption?" begins "well, it depends when," it is a series.
Declaring and reading a series
version 0.1
model "curves-solar"
time calendar monthly from 2026-01 for 36
entity asset solar : Asset.Real
// A price deck: dollars per MWh, stated at the dates the desk stated them.
assume power_price = curve linear {
2026-01: 42.10
2027-01: 44.75
2028-01: 46.20
}
// Revenue follows the deck period by period, without restating it.
stream solar.energy_revenue on entity asset.solar inflow currency USD {
schedule every month from 2026-01 to 2028-12
amount = 1200 * inputs.power_price
}
stream solar.om_expense on entity asset.solar outflow currency USD {
schedule every month from 2026-01 to 2028-12
amount = 9000
}A series has a name, an interpolation mode, and dated points. It is read like any assumption, inputs.power_price, and the value is the one at the reader's own period, so the stream tracks the deck as the clock advances. To read it at another date — next year's price, for a forward sale — write curve_value(inputs.power_price, date). The revenue line reads aloud as the deal actually works: "1,200 megawatt-hours a month, at the deck price prevailing that month."
Notice what the series replaced. Without it, the deck lives inside the amount as nested if(time.date < …) stanzas, or worse, as a pre-blended average price that no desk ever quoted. With it, the deck is a document within the document — five lines a trader can check against their own screen, separate from the operational claim of how many megawatt-hours you sell — and it sits on the review page beside every other assumption, with a source if you cite one.
Interpolation is a claim, not a detail
Between stated points, the mode decides the value, and the two modes are two different assertions about the world:
linear— the value glides between points, by calendar day. Right for prices, rents, and anything quoted as a trend: the deck above claims price rises smoothly through 2026, not that it jumps on New Year's Day.step— the value holds flat until the next point (flat-forward). Right for administered and contractual rates: a central-bank rate is 4.25% until the meeting that changes it; a contractual escalator is this year's rent until the anniversary.
Choose per series, deliberately. A stepped price deck understates every mid-year sale in a rising market; a linear policy rate invents fifty basis points of drift no committee ever voted. Same five points, different economics — the mode is part of the claim. step is the default; write linear when the glide is what you mean.
Outside its points, a series clamps: before the first point it returns the first value, after the last it returns the last. No extrapolation — a series asserts nothing beyond what was stated, so a model that runs past its deck holds the last stated value rather than inventing a trend. A series can also say where its claim stops: assume power_price = curve linear to 2028-12 { … } states the dates the series is good for, and a read past them is not a clamp but a refused run. Without a to, a reader that runs past the last point is warned once, because a held value is right for a rate deck and wrong for a depreciation table, and only you know which this is. If flat-forever is the wrong claim for your deal's tail, the fix is to make a claim: state more points, or state the end.
More a series can state
A series takes every clause a point assumption takes, and two of its own:
assume sofr : rate = curve from 2026-01 to 2029-12 {
2026-01: 0.048
2027-01: 0.042
2028-01: 0.039
} within [0.0, 0.10]
source { publisher "CME" series "SOFR" as_of 2026-01-02 }- Effective dates.
from <date>andto <date>on the header state the dates the series is good for. A read outside them refuses the run, naming the series, the date and the reader. Every point must lie inside them. - A type and
within. The type gives every point its domain, andwithinis checked on every point. - In a set. A series may be one field of a set (chapter 6), read by its full path:
inputs.market.rent_psf. - Supplied by the run. The run's inputs file can replace a series by its full name, with its own points, interpolation, effective dates and
source. A scalar parameter cannot replace a series. - As a discount rate. A run that names a series as
annual_discount_curvediscounts each period at that period's rate (chapter 3).
One place a bare read does not work: another assumption. An assumption is evaluated once, before any period exists, so it has no "reader's period" to read the series at. It reads the series at a stated date, curve_value(inputs.sofr, date(2026, 1, 1)); a bare inputs.sofr there is refused (E2313).
Series, value, or supplied fact?
Three forms exist for market-shaped inputs, and the boundaries are worth one paragraph each.
Series vs. value. assume power_price = 44.00 claims one price for all time — fine for a model whose horizon is short or whose price genuinely is contracted flat. The moment the claim varies by date, the flat value is not a simplification but a different (and usually indefensible) claim. Promote it: same statement, the curve body in place of the number, and every read of inputs.power_price now follows the date.
Series vs. a supplied fact. Both can carry market data. The line is when the data is known: a series is written into the model — the deck as of the underwriting date, part of the reviewed document. A bodiless assumption (assume sofr_fixing : rate) is filled at run time — this morning's fixing, supplied by the pipeline. A forward curve you underwrote on is a series; the fixing that resets this quarter's coupon is supplied by the run. Many models carry both, and the reader learns something true from which is which.
Series vs. expression. Escalation you can state as a rule — 3% per year, forever — belongs in an expression (pow, chapter 5). Escalation that follows a path someone quoted — a consultant's rent forecast with specific values in specific years — belongs in a series. The rule compresses; the path documents. When a reviewer asks "where did 46.20 come from," the answer "row three of the deck, dated 2028-01" is exactly the answer a series preserves and an expression buries.
One thing a series is not: stochastic. A series is a deterministic input — the deck, not the distribution around the deck. Uncertainty about its level is expressed the chapter-6 way, with a distributed assumption multiplying the read (inputs.price_factor * inputs.power_price), and chapter 12 makes that pattern do real Monte Carlo work.
What can go wrong
A duplicate point, or a duplicate name — refused at compile. A series with two values for one date is not a series, and two assumptions of one name are one too many.
The old spelling. curve_value("power_price", …) names a string, and a series is an assumption, named as one: the compiler refuses it and says to write inputs.power_price.
The clamp at the tail. The one that bites: a 10-year model over a 3-year deck runs seven years flat at the last stated price, by design. The run warns once per reader that ran past the deck (W5024), and the warning goes away when the series states its to date — which is the moment to ask whether the flat tail is a claim you are prepared to defend, or whether the reader should stop where the deck does. The habit that catches it is chapter 3's: open the series, look at the shape.
Exercises
Restore the deck
The starter blended a three-point rising price deck into one average number. Put the deck back.
- Declare a
linearseries namedpower_pricewith the quoted points:assume power_price = curve linear { … }. - Read the series at the period with
inputs.power_price.
Predict the direction of two changes before you run:
- The lifetime total: does a rising deck raise or lower it against the flat 44.35 blend?
- The NPV: the deck's cheap megawatt-hours come early and its dear ones late, and discounting cares about exactly that.
After the run, look at the tail. 2028's months hold flat at 46.20 — the
series' clamp at work, and the run warns once that revenue read past the
deck's last point. State to 2028-12 on the series to say the hold is meant,
and the warning goes.
Then, on your own:
- Change the exercise's series to
stepand predict which months change and in which direction before running. In one sentence: what does each mode claim happens between January and December? - Extend the model's horizon past the deck's last point and look at the flat tail in the series. Then add one more point stating a real tail claim, and watch the revenue follow it.