Heavy-Duty pricing of
Fixed Income financial contracts
with Julia


Felipe Noronha (@felipenoris)

Disclaimer

The views expressed here are solely those of the author and do not in any way represent the views of the Brazilian Development Bank.

My Background

  • Computer nerd
  • Interested in finance
  • Current job as Market Risk Manager

    • define and implement pricing models

    • execute pricing routines

  • Julia user for about 2 years

The Problem

  • price a big portfolio of fixed income contracts (Credit Portfolio)
  • ... fast!
  • 2.4 million contracts

  • 78 million cashflows

  • 17GB of uncompressed raw data in CSV format

  • more than US$ 150 billion in book value (2016 public balance sheet)

What was available

  • text editor (no dev tools)

The Results

20 minutes to build the database from CSV files (done once)

  • 17GB CSVs shrink to 6GB CSVs after removing unused columns
  • 6GB CSVs shrink to 4GB julia compressed native files
  • Still a lot of room for improvement

3.5 minutes for the pricing routine

  • from an empty julia session to the price results for each contract

10x faster when comparing to corporate systems

not possible without Julia

Ideas to tackle the problem

  • minimize IO operations by using memory buffers (memoize-like)
  • make use of Julia's parallel computation features
  • minimize memory allocation by using iterators instead of vectors
  • minimize indirection by making use of immutable types
  • transform CSV input files into some efficient file format for reading

Solution Components

InterestRates.jl

BusinessDays.jl

Market Data module: database of historical prices

Contract module : provides a julia type for a Fixed Income contract and build the database of contracts from CSV files

Pricing module : pricing logic

InterestRates.jl

Pricing a fixed cashflow

     t=0           t=T

                    N
     ┌──────────────┘
     P

In general: $ P = N \times \text{discountfactor}(r, T) $

But, given $r$ and $T$, you must know the convention used to:

  • how to count time
  • how the function discountfactor is defined

DayCountConvention

  • Actual360 : (D2 - D1) / 360
  • Actual365 : (D2 - D1) / 365
  • BDays252 : bdays(D1, D2) / 252

bdays is the business days between D1 and D2 given a particular calendar of holidays

CompoundingType

Defines how discountfactor is implemented.

  • ContinuousCompounding $$ exp(-rt) $$
  • SimpleCompounding $$\frac{1}{(1 + rt)}$$
  • ExponentialCompounding $$\frac{1}{(1+r)^t}$$

t is given in years

Term Structure

A Term Structure of Interest Rates, also known as zero-coupon curve, is a function f(t) → y that maps a given maturity t onto the rate y of a bond that matures at t and pays no coupons (zero-coupon bond).

It's not feasible to observe prices for each possible maturity. We can observe only a set of discrete data points of the yield curve. Therefore, in order to determine the entire term structure, one must choose an interpolation method, or a term structure model.

Curve Methods provided by InterestRates.jl

  • <<CurveMethod>>
    • <<Interpolation>>
      • <<DiscountFactorInterpolation>>
        • CubicSplineOnDiscountFactors
        • FlatForward
      • <<RateInterpolation>>
        • CubicSplineOnRates
        • Linear
        • StepFunction
      • CompositeInterpolation
    • <<Parametric>>
      • NelsonSiegel
      • Svensson

Svensson

$$ r(\tau) = \beta_1 + \beta_2 \left( \frac{ 1 - e^{- \lambda_1 \tau} }{\lambda_1 \tau} \right) + \beta_3 \left( \frac{ 1 - e^{- \lambda_1 \tau} }{\lambda_1 \tau} - e^{- \lambda_1 \tau} \right) + \beta_4 \left( \frac{ 1 - e^{- \lambda_2 \tau} }{\lambda_2 \tau} - e^{- \lambda_2 \tau} \right)$$

$\tau$ is the maturity in years


In [1]:
using Plots; gr()
curve_date = Date(2017,3,2)
days_to_maturity = [ 1, 22, 83, 147, 208, 269,
                     332, 396, 458, 519, 581, 711, 834]
rates = [ 0.1213, 0.121875, 0.11359 , 0.10714 , 0.10255 , 0.100527,
0.09935 , 0.09859 , 0.098407, 0.098737, 0.099036, 0.099909, 0.101135]
plot(days_to_maturity, rates, markershape=:circle, linewidth=0, xlabel="maturity", ylabel="rates", legend=:none, ylims=(0, max(rates...)+0.05))


Out[1]:
0 500 0.00 0.05 0.10 0.15 maturity rates

In [2]:
# Pkg.add("InterestRates"); Pkg.add("BusinessDays")
using InterestRates, BusinessDays
const ir = InterestRates

method = ir.CompositeInterpolation(ir.StepFunction(), # before-first
                                   ir.CubicSplineOnRates(), #inner
                                   ir.FlatForward()) # after-last

curve_brl = ir.IRCurve("Curve BRL", # name
    ir.BDays252(:Brazil), # DayCountConvention
    ir.ExponentialCompounding(), # CompoundingType
    method, # interpolation method
    curve_date, # base date
    days_to_maturity,
    rates);

In [3]:
x_axis = collect((curve_date+Dates.Day(1)):Dates.Day(1):Date(2022,2,1))

y_axis = zero_rate(curve_brl, x_axis) # performs interpolation

plot(x_axis, y_axis, xlabel="maturity", ylabel="rates", ylims=(0, max(rates...)+0.05), legend=:none, linewidth=2)
plot!([ advancebdays(:Brazil, curve_date, d) for d in days_to_maturity ], rates, markershape=:circle, linewidth=0)


Out[3]:
2018-01-01 2019-01-01 2020-01-01 2021-01-01 2022-01-01 0.00 0.05 0.10 0.15 maturity rates

Memoize-like curve


In [4]:
fixed_maturity = Date(2018,5,3)
discountfactor(curve_brl, fixed_maturity) # JIT
@elapsed discountfactor(curve_brl, fixed_maturity)


Out[4]:
0.000119189

In [5]:
buffered_curve_brl = ir.BufferedIRCurve(curve_brl)
discountfactor(buffered_curve_brl, fixed_maturity) # stores in cache
@elapsed discountfactor(buffered_curve_brl, fixed_maturity) # retrieves stored value in cache


Out[5]:
3.1615e-5

BusinessDays.jl


In [6]:
using BusinessDays
bdays(:Brazil, Date(2017,6,22), Date(2017,6,26))


Out[6]:
2 days

In [7]:
using BusinessDays
const bd = BusinessDays
d0 = Date(2015, 06, 29) ; d1 = Date(2100, 12, 20)
cal = bd.Brazil()

@elapsed bd.initcache(cal)


Out[7]:
0.087077417

In [8]:
# this same benchmark takes 38 minutes to complete on QuantLib
@elapsed for i in 1:1_000_000 bdays(cal, d0, d1) end


Out[8]:
0.281272911

Pricing Design

price(model, contract)

  • model has a type for multiple-dispatch to the correct price implementation

  • model has all market data / historical data needed for the price method. There's no IO operation when executing the price method.

  • contract is a data structure for the contract definition.

Pricing Steps

# contract search
contract = get_contract(db, code)

# get a unique identifier for the pricing model
model_key = infer_pricing_model_key(pricing_date, contract)

# creates an instance of the model.
# Connects to a database to retrieve market data (IO operation)
# This is done only once for each distinct value of model_key
model = get_pricing_model(conn, model_key)

# pricing formula
price(model, contract)

Contract type

  • That will depend on your data

  • A toy example:

type Contract
    id::Int
    currency::Symbol
    cashflows::Vector{CashFlow}
end

immutable CashFlow
    date::Date
    value::Float64
end

Price method

For fixed contracts, we can use the contract cash-flow directly, if available.

function price(model::FixedBond, c::Contract)
    mtm = 0.0

    for cf in c.cashflows       
        mtm += cf.value
            * discountfactor(model.curve_riskfree, cf.date) 
            * discountfactor(model.curve_spread, cf.date)
    end

    return mtm * model.currency_spot_value
end

In the more general case, for floating-rate contracts, we must project cashflow using interest rate curves

function price(model::FloatingRate, c::Contract)
    mtm = 0.0

    for (dt, v) in CashFlowIterator(model, c)
        mtm += v 
            * discountfactor(model.curve_riskfree, dt) 
            * discountfactor(model.curve_spread, dt)
    end

    return mtm * model.currency_spot_value
end

Julia's native serializer with GZip


In [9]:
type MyType
    a::Int
    b::Char
end

vec = [ MyType(1,'a'), MyType(2, 'b') ]


Out[9]:
2-element Array{MyType,1}:
 MyType(1,'a')
 MyType(2,'b')

In [10]:
using GZip
filename = "my_type_vec.native"
out = GZip.open(filename, "w")
serialize(out, vec)
close(out)


Out[10]:
0

In [11]:
;ls


custom.css
LICENSE
logo-64x64.png
Makefile
my_type_vec.native
slides.ipynb
slides.slides.html

In [12]:
@assert isfile(filename)
input = GZip.open(filename, "r")
vec = deserialize(input)
close(input)

vec


Out[12]:
2-element Array{MyType,1}:
 MyType(1,'a')
 MyType(2,'b')

Pricing in parallel

# maps contract id to a contract instance
const DictContract = Dict{Int, Contract}

# a subset of contracts mapped to a single file on disk
type Chunk
    dt::Date # database base date
    chunk_id::UInt16 # identifier for the chunk
    is_loaded::Bool
    d::DictContract
end

# database of contracts
# when created, loads contract_index from disk
type ContractDB
    dt::Date # database base date
    chunks::Dict{UInt16, Chunk} # chunk_id to chunk
    contract_index::Dict{Int, UInt16} # contract id to chunk_id
end

type ParallelContractDB
    db::ContractDB
    worker_pids::Vector{Int} # bounded workers
    chunk_to_worker_pids::Dict{UInt16, Int} # chunk_id to worker pid
    worker_pids_to_chunks::Dict{Int, Vector{UInt16}} # worker pid to a vector of chunks
end
function price_all(pd::ParallelContractDB, dt::Date)
    v_ids = Vector{Int}() # contract ids
    v_mtms = Vector{Float64}() # price results
    futures = Vector{Future}()

    for p in pd.worker_pids
        f = @spawnat p price_all_my_chunks(dt)
        push!(futures, f)
    end

    for f in futures
        v_ids_i, v_mtms_i = fetch(f)
        append!(v_ids, v_ids_i)
        append!(v_mtms, v_mtms_i)
    end

    return v_ids, v_mtms
end

Conclusion

  • Julia is closing the gap between business users and IT developers
  • opportunities in banking industry for Julia