Basics

Regime chains

rl.RegimeChain(generator)

The hidden regime is a continuous-time Markov chain given by its generator matrix Q (rows sum to zero, off-diagonal entries are the switching rates). Any number of regimes is allowed.

chain = rl.RegimeChain([[-5.0, 3.0, 2.0],
                        [4.0, -9.0, 5.0],
                        [1.0, 6.0, -7.0]])
chain.numberOfRegimes()          # 3
chain.stationaryDistribution()   # the long-run occupation of each regime
chain.meanHoldingTime()          # n / -trace Q, the expansion parameter
rl.RegimeChain.twoState(rate12, rate21)

The two-regime chain with switching rate rate12 out of regime 0 and rate21 out of regime 1.

chain = rl.RegimeChain.twoState(3.0, 5.0)

Per-regime parameters

A model parameter that may switch is given as a list with one value per regime, or as a scalar for the same value in every regime. The frozen limit — every regime equal — is the QuantLib model with those parameters, whatever the chain does; the certificates check each model against QuantLib in that limit.

rl.SwitchingVasicek(chain, r0=0.03, a=0.5, b=[0.06, 0.02], sigma=0.012)   # b switches, sigma does not

Maturities and dates

Times are years. Wherever a maturity is expected, a QuantLib Date is also accepted and is measured from ql.Settings.instance().evaluationDate with Actual/365 unless a dayCounter is given; a VanillaOption takes its maturity from a QuantLib exercise object.

ql.Settings.instance().evaluationDate = ql.Date(1, 1, 2020)
bond = rl.ZeroCouponBond(ql.Date(1, 1, 2025))
option = rl.VanillaOption(ql.PlainVanillaPayoff(ql.Option.Put, 100.0),
                          ql.EuropeanExercise(ql.Date(1, 1, 2021)))

Instruments, engines and results

instrument.setPricingEngine(engine)
instrument.NPV()
instrument.delta(), instrument.gamma(), instrument.theta(), instrument.vega(), instrument.rho()

As in QuantLib, an instrument holds its terms, an engine holds the model and the method, and NPV() triggers the calculation. The greeks are computed by differentiating the pricing formula, never by bumping; a greek the engine does not provide raises RuntimeError("... not provided by the engine"), as QuantLib’s “not provided” does.

  • Options under the characteristic-function engines: delta, gamma, theta, rho in the spot, and vega in the initial variance for stochastic-volatility models.

  • Bonds under Vasicek and CIR: delta and gamma in the short rate.

  • Options on the finite-difference engine: delta, gamma, theta from the grid.

option.setPricingEngine(rl.NumericalSwitchingEngine(model, regime=1))
option.NPV()
option.delta()
option.impliedVolatility()            # the Black volatility reproducing the price

Regimes and the starting regime

Every engine takes regime, the regime in force today. The price of the same instrument differs by starting regime; the difference is the memory of the regime, which the expansion carries in its initial-layer terms.

for regime in range(chain.numberOfRegimes()):
    option.setPricingEngine(rl.FastSwitchingEngine(model, order=4, regime=regime))
    print(regime, option.NPV())

Warnings and diagnostics

class rl.ExpansionWarning

The expansion is asymptotic in the mean holding time times the forcing, so it can fail to converge: for a slow chain, a large difference between regimes, or at high Fourier frequencies. FastSwitchingEngine raises an ExpansionWarning when the last term kept is not smaller than the one before it, when it is more than a thousandth of the value, or when nodes of the Fourier integral had to be solved numerically because the series diverged there. instrument._result("diagnostics") returns the holding time, the order used, the relative size of the last term and the count of numerical nodes.

import warnings
with warnings.catch_warnings(record=True) as caught:
    warnings.simplefilter("always")
    price = option.NPV()
for w in caught:
    print(w.message)
option._result("diagnostics")