The stateless design
Every price nanoql computes is a function of its inputs: the instrument, the evaluation context and the market data the engine takes. Nothing outside them reaches the result, and no call changes what a later one returns. So the same inputs give the same result, bit for bit, whenever and on whichever thread they are priced.
Keeping a book's prices current as its market data moves is the application's part: it knows which inputs changed and what depends on them, and calls the library again. This page shows the patterns it builds that layer from. Each sample prices a ten-year Euribor swap on the curve of the curves guide.
Rebuild on change
When a quote moves, build the objects that depend on it again from the new inputs, and price on them. The old curve and the prices made on it stand as they were.
// A swap is a value: its price is a function of the swap, the context and the curve.
static double Price(YieldTermStructure curve, EvaluationContext context)
{
VanillaSwap swap = new MakeVanillaSwap(new Period(10, TimeUnit.Years), new Euribor6M(curve))
.WithFixedRate(0.027)
.WithNominal(10e6)
.ToVanillaSwap(context);
return DiscountingSwapEngine.Calculate(swap, context, curve).NPV!.Value;
}
// A quote moves: build the curve again from the new quotes, and price on it.
double before = Price(Curves.Build(), context);
double after = Price(Curves.Build(shift: 0.0001), context);
| Quotes | NPV |
|---|---|
| as quoted | 0.00 |
| +1 bp | 8,721.66 |
Memoize by inputs
The same inputs build the same curve, so an application can keep what it has built under a key made of its inputs and share it: an immutable object is safe to read from any number of threads.
// The same inputs give the same curve, so a cache keyed by the inputs can share it, across threads too.
var curves = new ConcurrentDictionary<double, YieldTermStructure>();
YieldTermStructure CurveFor(double shift) => curves.GetOrAdd(shift, s => Curves.Build(s));
Price scenarios in parallel
Scenarios are independent calls on immutable inputs, so they run side by side with no locks:
double[] shifts = [-0.0050, -0.0025, -0.0010, 0.0, 0.0010, 0.0025, 0.0050];
double[] npvs = new double[shifts.Length];
Parallel.For(0, shifts.Length, i => npvs[i] = Price(CurveFor(shifts[i]), context));
| Shift | NPV | Change |
|---|---|---|
| -50 bp | -447,832.36 | -447,832.36 |
| -25 bp | -221,007.55 | -221,007.55 |
| -10 bp | -87,715.64 | -87,715.64 |
| 0 bp | 0.00 | 0.00 |
| +10 bp | 86,811.36 | 86,811.36 |
| +25 bp | 215,355.44 | 215,355.44 |
| +50 bp | 425,218.79 | 425,218.79 |
Run again, in any order or on any number of threads, the scenarios give the same bits.
The one global
ConventionProvider.Default, the conventions the library's presets and builders take when nothing else is given, is
the one global value (conventions.md). It is read when an object is built, never while it prices:
an object keeps the conventions it was built with, and a change of the default reaches the objects built after it.
Set it once at start-up, or open a scope with ConventionProvider.Use for one flow's builds; a scope flows with
asynchronous calls, as a culture does.