Rufineer / Docs / Cookbook Cookbook
Edge cases The awkward parts of real models, one recipe each: the problem in plain words, the Rufineer that solves it, and the model where it came up. Written in the planned language: many recipes don't compile yet.
Time COD in the middle of a period Operations start on 15 May but the columns are quarterly, so the first quarter should only earn about half a quarter of revenue.
Rufineer Copy
revenue : USD = energy * tariff * operations .shareoperations.share is the fraction of the period's days inside the phase: 1 in full periods, about 0.52 in that first quarter, 0 outside. A plain | during operations treats the quarter as fully operating.
Monthly columns for the first operating year Lenders want to see the ramp-up month by month, then quarterly for the rest of the loan.
Rufineer Copy
timeline {
start 2027-01-01
construction 18m step month
operations 25y step month for 12m , then quarter
}The ruler changes width once. Every line converts its annual rates with the right year fraction in each column.
Monthly before COD, six-monthly afterSeen in Coral Bay, Oriole The model runs monthly through development and construction and switches to half-years at commercial operation.
Rufineer Copy
timeline {
start 2018-01-01
development 12m
construction 3m
operations 25y
step month until operations .start, then half
}The switch point can be any date or phase start.
Phases with their own datesSeen in Oriole Three solar phases reach financial close a year apart, each with its own construction period and 25-year life.
Rufineer Copy
timeline {
start 2019-01-01
for s in stages {
construction [s] from inputs .stages[s].financial_close for inputs .stages[s].build
operations [s] 25y
}
}
module phase [s] "Phase {s}" for s in stages { … }Each stage gets its own flags and counters, and each module declared for s in stages becomes one sheet per stage.
A payment on one date A USD 250k licence fee falls due on 15 June 2031, inside a quarter.
Rufineer Copy
licence_fee : USD = 250_000 USD | at 2031-06-15 The amount lands in the column that contains the date.
Escalation on an anniversary monthSeen in Oriole The PPA tariff steps up every August from COD, not in January.
Rufineer Copy
tariff : USD/MWh = inputs .tariff | grow inputs .escalation yearly in Aug from operations .startyearly makes a step once a year; without it, the index grows every period.
Actuals for past periods An operating model needs recorded revenue for the first two years and forecasts after that.
Rufineer Copy
revenue : USD = energy * tariff | actuals "Revenue" until 2026-06-30 Past periods read the Actuals sheet of the data workbook by row label. The formula row is still there, so the switch-over is visible.
Monthly operations, six-monthly debt, yearly taxSeen in PPP road (Dakar) Traffic is modelled monthly, debt is paid in May and November, and tax is yearly.
Rufineer Copy
ruler monthly month
ruler finance half ends May, Nov
ruler annual year ends Dec
module traffic "Traffic" on monthly { … }
module debt "Debt" on finance { … }Revenue read by the debt module is summed into each half-year for you.
A daily loanSeen in Daily Bond A development loan accrues interest daily with draws on specific dates.
Rufineer Copy
ruler daily day from inputs .advance_date
module bridge "Bridge loan" on daily {
balance loan : GBP { + draws + capitalised - redemption }
interest : GBP = loan .opening * 12 %/y | day_count act/365
}A rate that resets with a delaySeen in Aframax tanker Charter rates follow the market with a three-month lag.
Rufineer Copy
charter_rate : USD/d = inputs .market_rate | lag 3m Days of an outage inside a monthSeen in Aframax tanker A 25-day dry-dock starts on the 20th and runs into the next month.
Rufineer Copy
period drydock [k] = inputs .drydocks[k].start for inputs .drydocks[k].days
off_hire : d = sum (overlap (drydock[*]))
charter_days : d = period .days - off_hireMoney A debt currency chosen by a switchSeen in Coral Bay The loan can be in pesos or dollars. Everything that touches it must follow the choice.
Rufineer Copy
debt_currency = option MXN | USD
balance loan : inputs .debt_currency {
+ funding .debt in inputs .debt_currency
- principal
}
debt_service_usd : USD = (interest + principal) in USDA unit can be an option. Conversions follow the switch through the fx row.
FX forecast from inflationSeen in Coral Bay The peso is forecast from purchasing-power parity, with a separate real change on top.
Rufineer Copy
fx MXN/USD = ppp (17.53 , local : inputs .macro.mx_cpi, foreign : inputs .macro.us_cpi)
* grow (inputs .macro.real_fx_change)Returns in three termsSeen in Coral Bay The summary shows project IRR in dollars, in pesos and in real dollars.
Rufineer Copy
irr_usd = xirr (project_cash)
irr_mxn = xirr (project_cash in MXN)
irr_real = xirr (real (project_cash, by : inputs .macro.us_cpi, base : timeline .start))P90 and P99 from a standard deviationSeen in Coral Bay The yield report gives a P50 and an uncertainty of 6%. The model needs P90 and P99.
Rufineer Copy
p90 : kWh/kWp/y = pvalue (inputs .p50, sd : 6 % , level : 90 ) // 1,985.5 → 1,832.8
p99 : kWh/kWp/y = pvalue (inputs .p50, sd : 6 % , level : 99 ) // 1,985.5 → 1,708.4 Written as p50 × (1 − NORM.S.INV(level) × sd) in the workbook.
A levelised provisionSeen in Coral Bay Inverters are replaced every 10 years. The model books a flat yearly provision with the same present value.
Rufineer Copy
replacements : USD/kW = inputs .inverter_cost | grow 1.5 %/y | every 10y from operations .start
provision : USD/kW/y = level (replacements, rate : 10 %/y ) // 4.17 USD/kW/y level also adds a check that the two NPVs match, like the workbook's Proof rows.
Capture price from hourly dataSeen in Big Model with Battery Prices and solar output come as 8,760-hour profiles; the model needs a yearly price.
Rufineer Copy
capture_price : USD/MWh = weighted_avg (inputs .prices_8760[year], by : inputs .solar_8760)Hour-of-year tables are reduced to yearly factors; the timeline stays monthly or longer.
Profit oil on an R-factorSeen in Oil PSC The contractor's share of profit oil falls as cumulative revenue grows relative to cumulative cost.
Rufineer Copy
r_factor = cumulative (contractor .revenue) / cumulative (contractor .cost)
profit_share = bands (prev (r_factor), { ..1.0 : 50 , 1.0 ..1.5 : 45 , 1.5 ..2.0 : 40 , 2.0 ..: 30 } %)prev uses last period's R-factor, as most contracts do, which also avoids a loop.
Debt Size on two cases at onceSeen in Coral Bay Debt is the lowest of 80% of cost, P50 CFADS at 1.35x and P99 CFADS at 1.00x.
Rufineer Copy
by_p50 : USD = npv (cash .cfads / 1.35 x | during tenor, rate : all_in)
by_p99 : USD = npv ((cash .cfads under p99) / 1.00 x | during tenor, rate : all_in)
solve size : USD = min (0.8 * capex .total, by_p50, by_p99)under p99 builds the P99 cash flows on a parallel sheet. No second copy of the model to keep in step.
Several tranches with their own termsSeen in Oriole Senior 1, 2 and 3 and a mezzanine loan each have their own share, tenor, rate, fees and repayment profile.
Rufineer Copy
for t in inputs .tranches {
loan [t] = term_loan (
amount : inputs .tranches[t].share * senior_size,
rate : inputs .tranches[t].rate,
window : operations .start + inputs .tranches[t].grace for inputs .tranches[t].maturity,
profile : inputs .tranches[t].repayment
)
}
debt_service : USD = sum (loan[*].debt_service)The terms come from a sheet of the data workbook. Adding a tranche is adding a row.
One facility on a different day countSeen in Oriole The model is Actual/365, but one loan accrues on 360 days.
Rufineer Copy
interest : USD = loan .opening * rate | day_count act/360 A fee that includes itselfSeen in Coral Bay The development fee is 5% of total cost, and total cost includes the fee.
Rufineer Copy
function gross_up (base, rate) = base * rate / (1 - rate)
development : USD = gross_up (epc .total, 5 % ) // 708.58 → 37.29 USD/kW Solve it with algebra, not a loop.
DSRA funded in cash or by a letter of creditSeen in Oriole A switch decides whether the reserve is cash on the balance sheet or an LC with a fee.
Rufineer Copy
when inputs .dsra_mode == funded {
dsra = reserve (target : debt_service | next 6m )
}
when inputs .dsra_mode == letter_of_credit {
lc_fee : USD = (debt_service | next 6m ) * inputs .lc_fee_rate
}A borrowing base redetermined twice a yearSeen in Oil RBL The reserve-based loan may not exceed 45% of the present value of remaining reserves.
Rufineer Copy
borrowing_base : USD = 45 % * pv_ahead (net_revenue, rate : 8 %/y ) | sampled every 6m
mandatory : USD = max (0 , rbl .opening - borrowing_base)A bid tariff at a target returnSeen in PPP road (Dakar) The concession is bid at the toll that gives equity 15%.
Rufineer Copy
seek inputs .toll so that returns .equity_irr = 15 % A Seek macro with a check, or algebra when the return is linear in the toll.
Refinancing a mini-perm A seven-year mini-perm with a 60% balloon is refinanced by a sculpted term loan.
Rufineer Copy
mini = term_loan (amount : debt .size, rate : 6 %/y , window : 7y , profile : balloon (60 % ))
refi = refinance (mini, at : mini .window.end,
with : sculpted_loan (rate : 6.5 %/y , window : 15y , cfads : cash .cfads, dscr : 1.30 x ))Equity Up-front equity, then pro rataSeen in Oriole Sponsors put in 40% of their equity before any debt is drawn, then fund pro rata with the lenders.
Rufineer Copy
funding uses {
first equity up to 40 % * equity .commitment
then pro_rata {
senior share 75 %
equity rest
}
}Planned as a drawdown order (equity_first 40 % of equity, then pro_rata); written by hand for now.
A partner with a carried interestSeen in Oriole The state utility holds 5% of the project without putting in capital.
Rufineer Copy
split to_equity {
main_investor 95 %
edm 5 % carried
funded by equity_draws
}
main_irr = xirr (to_equity .main_investor)The main investor funds all draws and receives 95% of distributions, which is why its IRR sits below the project's equity IRR.
A promote above a hurdle Investors take everything until they earn 12%, then the developer gets 20%.
Rufineer Copy
split to_equity {
first investors 100 % until investors earn 12 %/y
then investors 80 % , developer 20 %
}A tax equity yield flipSeen in Tax equity course model The investor takes 95% of cash and income until it earns its target yield, then 15%.
Rufineer Copy
split partnership among tax_investor, sponsor {
cash = cash .distributable tax_investor 95 % until flip, then 15 %
income = tax .taxable_income tax_investor 95 % until flip, then 15 %
}
event flip = first (earned (partnership .tax_investor.after_tax, 5 %/y ))Structure Features on a switchSeen in Coral Bay Inverter replacement is modelled but switched off in the base case.
Rufineer Copy
when inputs .include_inverters {
inverter_provision : USD = capacity * provision
}Rows stay in place when the switch changes; switched-off lines read as zero.
A construction S-curve from a distributionSeen in Oriole Spend follows a beta curve with alpha 9 and beta 2 rather than a typed table.
Rufineer Copy
spend : USD = spread (epc .total, over : construction , profile : beta (9 , 2 ))Also even, normal(mid, sd) or a table from the data workbook.
The whole model on one sheetSeen in Coral Bay A small model is easier to read on a single sheet, like Coral Bay's Financial Model.
Rufineer Copy
model "Coral Bay Solar" {
sheets one
}Modules become titled sections of one sheet, and the Imports blocks disappear because nothing crosses a sheet.
A formula Rufineer cannot express A lender insists on a specific Excel construct.
Rufineer Copy
special : USD = excel "=MAX(0,{rev}-{cost})*{flag}"
with rev: ops .revenue, cost: ops .opex, flag: operations References still go through the sheet's Imports block. rfn check lists every excel line.
Maths you repeat Several costs are quoted per kW and converted the same way.
Rufineer Copy
function per_kw (amount, capacity) = amount / capacity
om : USD = per_kw (inputs .om_budget, 1 kW ) * plant .capacityFunctions are written out in full where they are used, so there is nothing hidden in VBA.
One template sheet per assetSeen in Telecom Portfolio Sixteen cell towers share one template; some are excluded from the consolidation.
Rufineer Copy
module tower [t] "Tower {t}" for t in inputs .towers where inputs .towers[t].include == yes { … }
ebitda : USD = sum (tower[*].ebitda)Overhaul by running hoursSeen in Merchant Plant Model A gas turbine is overhauled every 25,000 operating hours, and output falls during the outage.
Rufineer Copy
running_hours : h = period .days * 24 h/d * capacity_factor
overhaul : USD = inputs .overhaul_cost | every 25_000 h of running_hoursFunding Debt pro rata with the equity, shareholders in orderSeen in Tutorial 2 (Mopane) The senior loan pays its share of every quarter's uses from financial close and is fully drawn at COD; preference shares, a shareholder loan and the ordinary shares pay the rest in order of seniority, each up to its commitment.
Rufineer Copy
drawdown {
before financial_close: ordinary
from financial_close: pro_rata on uses
equity in_order [pref_a, shl, ordinary]
}
senior = loan (size : 70 % of costs, interest_during_construction : paid, …)
shl = loan (by : shareholders, size : inputs .shl_size, …)
shares pref_a up to inputs .pref_a
shares ordinary = rest In the model block. The loan's share is its size over the uses from financial close, solved when the uses include interest and fees on what it draws.
Equity first, then the loanSeen in Tutorial 3 (Msasa) The partners pay their 40% before the bank lends anything, so the bank's money goes in last.
Rufineer Copy
drawdown equity_first
loan = loan (size : 60 % of uses, signed : base_case, upfront_fee : 1.25 % , …)
shares partners = rest The rest's amount is the uses less every other source; it pays that first, then the loans side by side, then anything still left. in_order [partners, loan, overrun] puts a capped class first and an overrun last.
Back-ended equity The lenders fund construction to their commitment first; the sponsors' equity comes in at the end.
Rufineer Copy
drawdown debt_firstback_ended says the same. The loans draw side by side, by their sizes, then the equity in order.
Two lenders side by side, a mezzanine first The mezzanine funds first; the commercial bank and the DFI then lend pari passu; the sponsors pay the rest.
Rufineer Copy
drawdown {
from financial_close: in_order [mezz, [senior, dfi]]
}Sources in [ ] share what is left by their sizes. The rest comes last, named or not. Drawing order is separate from repayment rank (rank:).
A loan sized on every use, fees and IDC includedSeen in By hand (Highveld) The lenders size the loan at 75% of total project cost, which includes the loan's own arrangement fee and interest during construction.
Rufineer Copy
senior = loan (size : 75 % of uses, upfront_fee : 2 % , commitment_fee : 0.5 %/y ,
interest_during_construction : paid, …)of costs is the capex before financing costs, with no loop. of uses is circular, and Rufineer writes the total as a solve line.