Cephei.QL

Fast Quant library using Cell Framework

Posted by steve on August 27, 2020

Overview

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:

  1. IPricingEngine is added to the constructor of Instruments that use a pricing engine to follow a functional idiom
  2. Evaluation Date is added to the constructor of Instruments so that a common source of date is available to all properties/functions that need a date for pricing, and is common across all instruments that will be valued together.
  3. Methods that do not return a value, instead return the reference object to allow them to be chained together as fluent function (e.g. FedFund.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.

Rational

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

Purpose

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.

Usages

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:

  • Parameters : references to the source cells.
  • Functions : declarative functions that perform calculations using other cells within the model.
  • Externally visible binding: cells and cell functions that can be bound and serialised to backing store.

Example

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.

Business Standards

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

Market Condition

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

Bond

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

Test
    [<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.