With Cephei.Cell we introduced a Cell Framework that allows computationally intensive problems to be declared as a series of functional definitions that the runtime calculates in parallel. Cephei.QL applies the Cell Framework to the QuantLib QLNet quantitative finance library to provide a series of pre-canned model building blocks for all Quantlib classes, that can be assembled into complete models for a financial instrument or portfolio. The full source is available on GitHub
Each Quant class is wrapped as a Model
, each property is wrapped as a ICell<>
with each function wrapped as a function taking ICell<>
parameters and returning an ICell<>
wrapper of the return value. There are three exceptions:
IPricingEngine
is added to the constructor of Instruments that use a pricing engine to follow a functional idiomFedFund
.AddFixing
return FedFund
)
Despite the overhead of constructing Cells rather than evaluating functions imperatively, the overall performance of a non-trivial model is significantly quicker because evaluation is performed in parallel with rendezvous happening when prices need to be aggregate.Quantlib is not a natural library for functional wrapping (because of the internal observer/observable pattern), Cephei.QL demonstrates that any library can be wrapped for functional definition.
Cephei.QL
(and Cephei.Cell
) encapsulates the dependencies within model, and only exposes value cells that can be edited and function cells that provide results that always reflect the value sources, irrespective of the change.
Financial Models that are presented as a dictionary of values with IObservale
/IObserver
event linkage can be wired together without needing to know internal structure can be used as financial building blocks
Cephei.QL provide a building block for Cephei.XL (that supports construction of models in Spreadsheets -that can be saved as F# code), and embedding in systems, including “Financial Digital Twins” for real-time-risk.
The Cephei.QLnuget package includes Cephei.QL assembly including 2000+ models and a cell
module that provides a functions to create models, together with Utility functions for cell
to construct cells with type inference and triv
for trivial (lookup) functions. Depending on the value of Cell.Lazy
(false for construction at definition time) and Cell.Parallel
(true for parallel execution).
Each model is declared with three sections:
This model is based on the QLLib Bond example that uses Cephei.QL
as blocks to represent a small portfolio of Fixed Rate Bonds, with difference {tenor, coupon rates, payment frequencies, and yield rates} and allows the {Face Value, Quantity, Redemption} to be edited and provides a Market Clean Price.
External to the Bond model, two models are provided for Business wide properties and market conditions that are used to change the valuation through event propergation.
type BusinessStandards () as this =
inherit Model ()
let accrualConvention = value BusinessDayConvention.Unadjusted
let paymentConvention = value BusinessDayConvention.ModifiedFollowing
let settlementDays = value 3
let dayCount = value (new ActualActual (ActualActual.Convention.ISMA) :> DayCounter)
let includeSettlementDate = value (new System.Nullable<bool> (true))
do this.Bind ()
member this.AccrualConvention = accrualConvention
member this.PaymentConvention = paymentConvention
member this.SettlementDays = settlementDays
member this.DayCount = dayCount
member this.IncludeSettlement = includeSettlementDate
type MarketCondition
( standards : BusinessStandards ) as this =
inherit Model ()
let toNullable (v : double) = new System.Nullable<double> (v)
let calendar = Fun.TARGET()
let clockDate = value Date.Today;
let convention = value BusinessDayConvention.Following
let today = calendar.Adjust clockDate convention
do this.Bind ()
member this.Today = today
member this.Calendar = calendar
member this.ClockDate = clockDate
This trivial model provides a clock date that is incremented in the text example, and calculates a cashflow date using a calendar and date adjustment convention. Whenever the clock date is changed, the update to Today
is sent to an cells dependant on this value
type BondPortfolio
( standards : BusinessStandards
, marketCondition : MarketCondition
) as this =
inherit Model ()
let calendar = triv (fun () -> marketCondition.Calendar.Value :> Calendar)
(* … *)
let makeBond issue length coupon (frequency : ICell<Period>) yieldVal =
let today = marketCondition.Today.Value
let dated = triv (fun () -> today) // don't reset on valuation date
let nullDate = value (null :> Date)
let maturity = marketCondition.Calendar.Advance1 dated length years standards.PaymentConvention eom
let schedule = Fun.Schedule dated maturity frequency calendar standards.AccrualConvention standards.AccrualConvention dateGenerationRule eom nullDate nullDate
let yieldCurve = triv (fun () -> (makeYield yieldVal))
let engine = Fun.DiscountingBondEngine yieldCurve standards.IncludeSettlement
let castEgnine = triv (fun () -> engine.Value :> IPricingEngine)
let exCouponPeriod = value (null :> Period)
let b = Fun.FixedRateBond standards.SettlementDays faceAmount schedule coupon bondDayCount standards.PaymentConvention redemption issue calendar exCouponPeriod calendar convention eom castEgnine marketCondition.Today
b.Mnemonic <- "B" + id.ToString()
id <- id + 1
b
let bonds =
seq {for l in lengths do
for c in coupons do
for f in frequencies do
for y in yields do
(l,c,f, y)}
|> Seq.map (fun (l,c,f, y) -> makeBond marketCondition.Today l c f y)
|> Seq.toArray
let cleanPrices = bonds |> Array.map (fun i -> i.CleanPrice)
let cleanPrice = cell (fun () -> cleanPrices |> Seq.fold (fun a y -> a + y.Value * quantity.Value) 0.0)
do this.Bind ()
member this.Amount = faceAmount
member this.Quantity = quantity
member this.Redemption = redemption
member this.CleanPrice = cleanPrice
This model uses the business standards and market conditions and a set of permutations to build Fixed Rate Bonds by constructing schedule, yield curve and engine.. the user of the model does not need to know the intermediate steps that Quantlib uses to build a Bond. Refactoring the Yield Curve functionality to be shared via market conditions is a simple task that is transparent to users
[<TestMethod>]
member this.TestLazy () =
let lots =
seq { for n in 1..60 do
new BondPortfolio (standards, market)}
|> Seq.toList
let r =
seq { for c in 0..100 do
market.ClockDate.Value <- market.ClockDate.Value + c
let cleanPrice = lazy (lots |> List.fold (fun a y -> a + y.CleanPrice.Value) 0.0 )
Console.WriteLine ("Lazy, {1}, {0}", cleanPrice.Value, market.Today.Value)
cleanPrice
} |> Seq.toArray
Assert.IsTrue(true);
The test case generates 60 portfolios, and retrieves the clean price for 100 time points, but the event could be market prices, or a what-if of changing settlement period. Enabling do Cell.Parellel <- true
reduced runtime by a factor of four on my workstation.
The key take-away is that the cost of profiling calculations (on background threads) still results in shorter runtime on multi-core computers that are now common.