In case anyone finds it useful, here it is. I employed backward induction, which means calibration time, and my solver is disgustingly primitive, but it is simple and it works. And it uses memory.

Usecase is:

public void Calculate() { var tree = new BinomialTree( new[] {0.0608, 0.0611, 0.0621, 0.0631, 0.0655}, new[] {0.00, 0.17, 0.16, 0.15, 0.13}); // now you can use tree.TreeRate(i,j) function to find rates in tree nodes }

And the code is:

public class Solver { public double? Solve(Func<double, double> what, double from, double to) { Debug.Assert(Math.Sign(what(from))!= Math.Sign(what(to)), "Function must be of different signs on borders"); do { var mid = (to + from)/2; var valOnTo = what(to); var valOnMid = what(mid); if (Math.Sign(valOnTo) != Math.Sign(valOnMid)) from = mid; else to = mid; } while (Math.Abs(from-to) > 10e-5); return (to + from) / 2; } } public class BinomialTree { private readonly double[] _treeRates; private readonly double[] _treeTerms; private readonly double[] _treeVols; private readonly int _n; /// <summary> /// Calculates interest rate in (i, j) node in tree /// </summary> /// <param name="i">number of time step</param> /// <param name="j">number of point by vertical axis, j = 0 is the highest, j = i is the lowest</param> /// <returns>Double value - an interest rate in (i, j) node in tree</returns> public double TreeRate(int i, int j) { Debug.Assert(i < _n && j < _n && j <= i, "Requested node is out of the tree"); return _treeRates[i]*Math.Exp(-2*_treeVols[i]*j); } public BinomialTree(double[] rates, double[] vols, double[] terms = null) { Debug.Assert(rates.Length == vols.Length, "Count of rates and volatilities must be equal"); _n = rates.Length; _treeRates = new double[_n]; _treeVols = vols; if (terms != null) { Debug.Assert(terms.Length == vols.Length, "Count of rates and volatilities must be equal"); _treeTerms = terms; } else { _treeTerms = new double[_n]; for (var i = 0; i < _n; i++) _treeTerms[i] = i+1; } _treeRates[0] = rates[0]; var solver = new Solver(); for (var i = 1; i < _n; i++) { // 1) calculating price of ZCB with term i and rate r[i] var price = Math.Exp(-rates[i]*_treeTerms[i]); var num = i; // to avoid using cycle variable in clojure // 2) finding rate which would make bond price equal to calculated price var rt = solver.Solve(rate => EvaluatePrice(rate, num + 1) - price, 0.00, 1.00); if (rt == null) throw new InvalidOperationException("Didn't converge!"); // 3) Saving result _treeRates[i] = rt.Value; } } private double EvaluatePrice(double rate, int num) { var oldPrices = new double[num + 1]; var newPrices = new double[num]; for (var i = 0; i < num + 1; i++) oldPrices[i] = 1; for (var n = num-1; n >= 0; n--) { for (var k = 0; k <= n; k++) { var rt = ((n == num - 1) ? rate : _treeRates[n]) * Math.Exp(-2 * _treeVols[n] * k); newPrices[n - k] = 0.5*(oldPrices[n-k] + oldPrices[n-k+1])*Math.Exp(-rt); } if (n > 0) for (var k = 0; k <= n; k++) oldPrices[k] = newPrices[k]; } return newPrices[0]; } }