Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Experimental re-write, but using Transducers #151

Merged
merged 38 commits into from
Jul 30, 2023
Merged

Conversation

alecloudenback
Copy link
Member

@alecloudenback alecloudenback commented Jun 10, 2023

This first section is a draft of a blog-post disucssing ecosystem updates.

FinanceModels.jl: The Evolution of Yields.jl

Yields.jl is now FinanceModels.jl

This re-write accomplishes three primary things:

  • Provide a composable set of contracts and Quotes
  • Those contracts, when combined with a model produce a Cashflow via a flexibily defined Projection
  • models can be fit with a new unified API: fit(model_type,quotes,fit_method)

1. Cashflow - a fundamental financial type

Say you wanted to model a contract that paid quarterly payments, and those payments occurred starting 15 days from the valuation date (first payment time = 15/365 = 0.057)

Previously, you had two options:

  • Choose a discrete timestep to model (e.g. monthly, quarterly, annual) and then lump the cashflows into those timesteps. E.g. with monthly timesteps of a unit payment of our contract, it might look like: [1,0,0,1,0,0...]
  • Keep track of two vectors: one for the payment and one for the times. In this case, that might look like: cfs = [1,1,...]; times = [0.057, 0.307...]

The former has inaccuracies due to the simplified timing and logical complication related to mapping the contracts natural periodicity into an arbitrary modeling choice. The latter becomes unwieldy and fails to take advantage of Julia's type system.

The new solution: Cashflows. Our example above would become: [Cashflow(1,0.057), Cashflow(1,0.307),...]

2. Contracts - A composable way to represent financial instruments

Contracts are a composable way to represent financial instruments. They are, in essence, anything that is a collection of cashflows. Contracts can be combined to represent more complex instruments. For example, a bond can be represented as a collection of cashflows that correspond to the coupon payments and the principal repayment.

Examples:

  • a Cashflow
  • Bonds:
    • Bond.Fixed, Bond.Floating
  • Options:
  • Option.EuroCall and Option.EuroPut
  • Compositional contracts:
    • Forwardto represent an instrument that is relative to a forward point in time.
    • Composite to represent the combination of two other instruments.

In the future, this notion may be extended to liabilities (e.g. insurance policies in LifeContingencies.jl)

Creating a new Contract

A contract is anything that creates a vector of Cashflows when collected. For example, let's create a bond which only pays down principle and offers no coupons.

using FinanceModels,FinanceCore

# Transducers is used to provide a more powerful, composible way to construct collections than the basic iteration interface
using Transducers: __foldl__, @next, complete

"""
A bond which pays down its par (one unit) in equal payments. 
"""
struct PrincpleOnlyBond{F<:FinanceCore.Frequency} <: FinanceModels.Bond.AbstractBond
    frequency::F
    maturity::Float64
end

# We extend the interface to say what should happen as the bond is projected
# There's two parts to customize:
# 1. any initialization or state to keep track of
# 2. The loop where we decide what gets returned at each timestep
function Transducers.__foldl__(rf, val, p::Projection{C,M,K}) where {C<:PrincpleOnlyBond,M,K}
    # initialization stuff
    b = p.contract # the contract within a projection
    ts = Bond.coupon_times(b) # works since it's a FinanceModels.Bond.AbstractBond with a frequency and maturity
    pmt = 1 / length(ts)

    for t in ts
        # the loop wich returns a value
        cf = Cashflow(pmt, t)
        val = @next(rf, val, cf) # the value to return is the last argument
    end
    return complete(rf, val)
end

That's it! then we can use this fitting models, projections, quotes, etc. Here we simply collect the bond into an array of cashflows:

julia> PrincpleOnlyBond(Periodic(2),5.) |> collect
10-element Vector{Cashflow{Float64, Float64}}:
 Cashflow{Float64, Float64}(0.1, 0.5)
 Cashflow{Float64, Float64}(0.1, 1.0)
 Cashflow{Float64, Float64}(0.1, 1.5)
 Cashflow{Float64, Float64}(0.1, 2.0)
 Cashflow{Float64, Float64}(0.1, 2.5)
 Cashflow{Float64, Float64}(0.1, 3.0)
 Cashflow{Float64, Float64}(0.1, 3.5)
 Cashflow{Float64, Float64}(0.1, 4.0)
 Cashflow{Float64, Float64}(0.1, 4.5)
 Cashflow{Float64, Float64}(0.1, 5.0)

Note that all contracst in FinanceModels.jl are currently unit contracts in that they assume a unit par value.

More complex Contracts

When the cashflow depends on a model. An example of this is a floating bond where the coupon paid depends on a view of forward rates. See section 6 on projections for how this is handled.

3. Quotes - The observed price we need to fit a model to

Quotes are the observed prices that we need to fit a model to. They represent the market prices of financial instruments, such as bonds or swaps. In the context of the package, a quote is defined as a pair of a contract and a price.

For example, a par yield bond paying a 4% coupon (paid as 2% twice per annum) implies a price at par (i.e. 1.0):

julia> ParYield(Periodic(0.04,2),10)
Quote{Float64, FinanceModels.Bond.Fixed{Periodic, Float64, Int64}}(
1.0, 
FinanceModels.Bond.Fixed{Periodic, Float64, Int64}(0.040000000000000036, Periodic(2), 10))

A number of convenience functions are included to construct a Quote:

  • ZCBPrice and ZCBYield
  • ParYield
  • CMTYield
  • OISYield
  • ForwardYields

4. Models - Not just yield curves anymore

  • Yield Curves: all of Yields.jl yield models are included in the initial FinanceModels.jl release
  • Equities and Options: The initial release includes BlackScholesMerton option pricing and one can use constant or spline volatility models
  • Others more to come in the future

Creating a new model

Here we'll do a complete implementation of a yield curve model where the discount rate is approximated by a straight line (often called an AB line from the y=ax+b formula.

 using FinanceModels, FinanceCore
 using AccessibleOptimization 
 using IntervalSets
 
struct ABDiscountLine{A} <: FinanceModels.Yield.AbstractYieldModel
    a::A
    b::A
end

ABDiscountLine() = ABDiscountLine(0.,0.)

function FinanceCore.discount(m::ABDiscountLine,t)
    #discount rate is approximated by a straight lined, floored at 0.0 and capped at 1.0
    clamp(m.a*t + m.b, 0.0,1.0) 
end


# `@optic` indciates what in our model variables needs to be updated (from AccessibleOptimization.jl)
# `-1.0 .. 1.0` says to bound the search from negative to postive one (from IntervalSets.jl)
FinanceModels.__default_optic(m::ABDiscountLine) = OptArgs([
    @optic(_.a) => -1.0 .. 1.0,
    @optic(_.b) => -1.0 .. 1.0,
]...)

quotes = ZCBPrice([0.9, 0.8, 0.7,0.6])

m = fit(ABDiscountLine(),quotes)

Now, m is a model like any of the other yield curve models provided and can be used in that context. For example, calculating the price of the bonds contained within our quotes where we indeed recover the prices for our contrived example:

julia> map(q -> pv(m,q.instrument),quotes) 
4-element Vector{Float64}:
 0.9
 0.8
 0.7
 0.6

5. fit - The standardized API for all models, quotes, and methods

       Model                                                               Method
          |                                                                   |
  	|------------|                                                     |---------------|
fit(Spline.Cubic(), CMTYield.([0.04,0.05,0.055,0.06,0055],[1,2,3,4,5]), Fit.Bootstrap())
                    |-------------------------------------------------|
                                              |
                                              Quotes
  • Model could be Spline.Linear(), Yield.NelsonSiegelSvensson(), Equity.BlackScholesMerton(...), etc.
  • Quote could be CMTYields, ParYields, Option.Eurocall, etc.
  • Method could be Fit.Loss(x->x^2), Fit.Loss(x->abs(x)), Fit.Bootstrap(), etc.

The benefit of this versus the old Yields.jl API is:

  • Without a generic fit method, no obvious way to expose different curve construction methods (e.g. choice of model and method)
  • The fit is extensible. Users or other packages could define their own Models, Quotes, or Methods and integrate into the JuliaActuary ecosystem.
  • The fit formulation is very generic: the required methods are minimal to integrate in order to extend the functionality.

Customizing model fitting

Model fitting can be customized:

  • The loss function (least squares, absolute difference, etc.) via the third argument to fit:
    • e.g.fit(ABDiscountLine(), quotes, FIt.Loss(x -> abs(x))
    • the default is Fit.Loss(x->x^2)
  • the optimization algorithm by defining a method FinanceModels.__default_optim__(m::ABDiscountLine) = OptimizationOptimJL.Newton()
  • you may need to change the __default_optic to be unbounded (simply omit the => and subsequent bounds)
  • The default is OptimizationMetaheuristics.ECA()
  • The general algorithm can be customized by creating a new method for fit:
function FinanceModels.fit(m::ABDiscountLine, quotes, ...)
   # custom code for fitting your model here
end
  • As an example, the splines (Spline.Linear(), Spline.Cubic(),...) are defined to use boostrap by default: fit(mod0::Spline.BSpline, quotes, method::Fit.Bootstrap)

6. Projections

A Projection is a generic way to work with various data that you can project forward. For example, getting the series of cashflows associated with a contract.

What is a Projection?

struct Projection{C,M,K} <: AbstractProjection
    contract::C    # the contract (or set of contracts) we want to project
    model::M       # the model that defines how the contract will behave
    kind::K           # what kind of projetion do we want? only cashflows? 
end

contract is obvious, so let's talk more about the second two:

  • model is the same kind of thing we discussed above. Some contracts (e.g. a floating rate bond). We can still decompose a floating rate bond into a set of cashflows, but we need a model.
    • There are also projections which don't need a model (e.g. fixed bonds) and for that there's the generic NullModel()
  • kind defines what we'll return from the projection.
    • CashflowProjection() says we just want a Cashflow[...] vector
    • ... but if we wanted to extend this such that we got a vector containing cashflows, capital factors, default rates, etc we could define a new projection type (e.g. we might call the above AssetDetailProjection()
    • Currently, only CashflowProjection() is defined by FinanceModels.jl

Contracts that depend on the model (or multiple models)

For example, the cashflows you generate for a floating rate bond is the current reference rate. Or maybe you have a stochastic volatilty model and want to project forward option values. This type of dependency is handled like this:

  • define model as a relation that maps a key to a model. E.g. a Dict("SOFR" => NelsonSiegelSvensson(...))`
  • when defining the logic for the reducible collection/foldl, you can reference the Projection.model by the assocated key.

Here's how a floating bond is implemented:

The contract struct. The key would be "SOFR" in our example above.

struct Floating{F<:FinanceCore.Frequency,N<:Real,M<:Timepoint,K} <: AbstractBond
    coupon_rate::N # coupon_rate / frequency is the actual payment amount
    frequency::F
    maturity::M
    key::K
end

And how we can reference the assocated model when projecting that contract. This is very similar to the definition of __foldl__ for our PrincipleOnlyBond, except we are paying a coupon and referencing the scenario rate.

@inline function Transducers.__foldl__(rf, val, p::Projection{C,M,K}) where {C<:Bond.Floating,M,K}
    b = p.contract
    ts = Bond.coupon_times(b)
    for t in ts
        freq = b.frequency # e.g. `Periodic(2)`
        freq_scalar = freq.frequency  # the 2 from `Periodic(2)`

        # get the rate from the current time to next payment 
        # out of the model and convert it to the contract's periodicity
        model = p.model[b.key]
        reference_rate = rate(freq(forward(model, t, t + 1 / freq_scalar)))
        coup = (reference_rate + b.coupon_rate) / freq_scalar
        amt = if t == last(ts)
            1.0 + coup
        else
            coup
        end
        cf = Cashflow(amt, t)
        val = @next(rf, val, cf)
    end
    return complete(rf, val)
end

In this post we've now defined two assets that can work seamlessly with projecting cashflows, fitting models, and determining valuations :)

7. ProjectionKinds

While CashflowProjection is the most common (and the only one built into the inital release of FinanceModels), a Projection can be created which handles different kinds of outputs in the same manner as projecting just basic cashflows. For example, you may want to output an amortization schedule, or a financial statement, or an account value roll-forward. The Projection is able to handle these custom outputs by dispatching on the third element in a Projection.

Let's extend the example of a principle-only bond from section 2 above. Our goal is to create a basic amortization schedule which shows the payment made and outstanding balance.

First, we create a new subtype of ProjectionKind:

struct AmortizationSchedule <: FinanceModels.ProjectionKind
end

And then define the loop for the amortization schedule output:

# note the dispatch on `AmortizationSchedule` in the next line
function Transducers.__foldl__(rf, val, p::Projection{C,M,K}) where {C<:PrincpleOnlyBond,M,K<:AmortizationSchedule}
    # initialization stuff
    b = p.contract # the contract within a projection
    ts = Bond.coupon_times(b) # works since it's a FinanceModels.Bond.AbstractBond with a frequency and maturity
    pmt = 1 / length(ts)
    balance = 1.0
    for t in ts
        # the loop wich returns a tuple of the relevant data
        balance -= pmt
        result = (time=t,payment=pmt,outstanding=balance)
        val = @next(rf, val, result) # the value to return is the last argument
    end
    return complete(rf, val)
end

We can now define the projection:

julia> p = Projection(
           PrincpleOnlyBond(Periodic(2),5.),  # our contract
           NullModel(),                       # the projection doesn't need a model, so use the null model
           AmortizationSchedule(),            # specifiy the amortization schedule output
           );

And then collect the values:

julia> collect(p)
10-element Vector{NamedTuple{(:time, :payment, :outstanding), Tuple{Float64, Float64, Float64}}}:
 (time = 0.5, payment = 0.1, outstanding = 0.9)
 (time = 1.0, payment = 0.1, outstanding = 0.8)
 (time = 1.5, payment = 0.1, outstanding = 0.7000000000000001)
 (time = 2.0, payment = 0.1, outstanding = 0.6000000000000001)
 (time = 2.5, payment = 0.1, outstanding = 0.5000000000000001)
 (time = 3.0, payment = 0.1, outstanding = 0.40000000000000013)
 (time = 3.5, payment = 0.1, outstanding = 0.30000000000000016)
 (time = 4.0, payment = 0.1, outstanding = 0.20000000000000015)
 (time = 4.5, payment = 0.1, outstanding = 0.10000000000000014)
 (time = 5.0, payment = 0.1, outstanding = 1.3877787807814457e-16)

8. Post-script

FAQ

  1. Why use Transducers?
    Transducers vastly simplified the iteration and state handling needed when projecting the contracts (see dead PR Experimental re-write and rename to FinanceModels.jl #150). The performance remains excellent and made a lot of the internals much simpler.

For the most part, this isn't user facing but where it is (e.g. extending to the functionality PrincpleOnlyBond, there's actually very little, very straightforward logic to implement for the user.


Work-in-progress notes

closes #145, #144, #42, #50

Same thing as #150 but using Transducers instead of iteration interface

Supersedes #146, which took a type-based approach to Composite structs while this takes more of a vector based approach to combining atomic cashflows.

Currently requires the associated dev branches of FinanceCore, Yields (aka FinanceModels), and ActuaryUtilities to work together. WIll be breaking change for Yields, TBD on ActuaryUtilities and FinanceCore. Associated PRs:

TODOs

  • bond.frequency.frequency is awkward, will make a getter function
  • Core contracts:
    • Composite contact (e.g. Fixed + Float -> Swap)
    • Forward contact
    • How to represent assumed 1 year forwards with contract API?
    • Derivatives?
    • distinguish between clean and dirty prices? (Created A way to distinguish clean vs dirty prices? #154 to address later)
  • Fitting
    • Document how to set default seed for solvers
    • Note that tolerance on NS/NSS tests widened to accommodate different solve routine
  • How to integrate Dates? (opened Allow handling of Dates #155 )
  • Core methods:
    • port Yields.jl methods
  • Precompile statements
  • Documentation
    • Doc string sweep
    • New Readme
    • Yields -> FinanceModels migration guide
    • Developer guide - how to create a new model (covered in Guide)
    • Example of different ProjectionKind
  • ActuaryUtilities compatibility. Integration tests currently failing

To get input on:

  • should the name be Instruments or Observablesor Contracts for the provided financial instruments?

  • Should the quotes (e.g. ParYield or ZCBPrice) have a quote in the name since they create quotes?

  • How to distinguish/indicate "the thing that needs to be fit"
    E.g. when you want to specify what kind of volatility model to fit a BSM to option quotes?

  • Use a non-initialized type? fit(BlackScholesMerton(0.01,0.02,ConstantVolatility),qs)

  • Use some form of "concrete" type but the model to be fit is represented by some "uninitialized" type (e.g. like SumTypes.jl

Ergonomics:

Package design:

  • promote pv to FinanceCore given its utility here
  • promote Cashflow up to FC

Maybe:

  • Refactor to use Transducers instead of iterators?
  • Change FinanceCore.CompoundingFrequency to just Frequency since it's used by Instruments for payment frequency?
  • Upstream the instruments/observables into separate package or FinanceCore
  • Use Weak Dependencies to, e.g. only pull in interpolation methods for Bootstrap (created Move some code to extensions #153)
  • Add UnitCashflow as a specialized Cashflow? (TBD Later if desired)

@codecov
Copy link

codecov bot commented Jun 10, 2023

Codecov Report

Merging #151 (30b5fe6) into master (e9b32dd) will decrease coverage by 96.35%.
The diff coverage is 0.00%.

❗ Current head 30b5fe6 differs from pull request most recent head ee1b96f. Consider uploading reports for the commit ee1b96f to get more accurate results

@@            Coverage Diff             @@
##           master    #151       +/-   ##
==========================================
- Coverage   96.34%   0.00%   -96.35%     
==========================================
  Files           9      11        +2     
  Lines         438     299      -139     
==========================================
- Hits          422       0      -422     
- Misses         16     299      +283     
Files Changed Coverage Δ
src/Contract.jl 0.00% <0.00%> (ø)
src/Projection.jl 0.00% <0.00%> (ø)
src/fit.jl 0.00% <0.00%> (ø)
src/model/Equity.jl 0.00% <0.00%> (ø)
src/model/Model.jl 0.00% <0.00%> (ø)
src/model/Spline.jl 0.00% <0.00%> (ø)
src/model/Volatility.jl 0.00% <0.00%> (ø)
src/model/Yield.jl 0.00% <0.00%> (ø)
src/model/Yield/NelsonSiegelSvensson.jl 0.00% <0.00%> (ø)
src/model/Yield/SmithWilson.jl 0.00% <0.00%> (ø)
... and 1 more

📣 We’re building smart automated test selection to slash your CI/CD build times. Learn more

@alecloudenback alecloudenback marked this pull request as draft June 17, 2023 04:47
@alecloudenback
Copy link
Member Author

How to represent assumed 1 year forwards with contract API?

I think through a ForwardQuote type?

@alecloudenback alecloudenback linked an issue Jun 19, 2023 that may be closed by this pull request
struct CashflowProjection <: ProjectionKind end

@inline function Transducers.__foldl__(rf, val, p::Projection{C,M,K}) where {C<:Cashflow,M,K}
for i in 1:1

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This confuses me a bit

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just pushed an update to that file with a bunch of commentary, hopefully that helps?

@alecloudenback
Copy link
Member Author

I updated the original post to include:

  • An example of creating a from-scratch custom model (ABDiscountLine)
  • More details in the fit section (section 5) about how fitting models can be customized

Seems like to integrate dates there can be a function that maps DatedCashFlow to CashFlow or something?

Actually, right now a Cashflow's time field can already be a Union{<:Real, Dates.Date) but the Date version has no current logic that supports it. Now that I've got everything pieced together, over the next couple of weeks I'll experiment with supporting dates. Maybe a wrapper type that just wraps models with a Date that represents what time zero is and what the day count convention is?

@MatthewCaseres
Copy link

I updated the original post to include:

  • An example of creating a from-scratch custom model (ABDiscountLine)
  • More details in the fit section (section 5) about how fitting models can be customized

Seems like to integrate dates there can be a function that maps DatedCashFlow to CashFlow or something?

Actually, right now a Cashflow's time field can already be a Union{<:Real, Dates.Date) but the Date version has no current logic that supports it. Now that I've got everything pieced together, over the next couple of weeks I'll experiment with supporting dates. Maybe a wrapper type that just wraps models with a Date that represents what time zero is and what the day count convention is?

If you find an API you really like for it then looking forward to using it. If it isn't clear how to handle it after some consideration then it is ok to ship something simple and save dates for a minor release after more usage reveals pain points in usage.

Did my best to look through all the code. Some of the things like for 1:1 I am suspicious of, but I trust if you say it
necessary for interop with transducers.

I probably don't have much time for review beyond the comments already made.

If I ever write a reasonable actuarial model, I will post in the discussion and ask about what types of analysis are necessary on the produced CashFlows.

@MatthewCaseres
Copy link

MatthewCaseres commented Jul 7, 2023

These thoughts aren't warranting any response, just thinking out loud. Overall the full example helped me see how to use the software. May be unable to respond for some time, this is too interesting and is making it hard to do homework.

Transducers

You say it isn't user-facing, but if I define a contract like -

function Transducers.foldl(rf, val, p::Projection{C,M,K}) where {C<:PrincpleOnlyBond,M,K}

  1. then it is user facing? When you say

In the future, this notion may be extended to liabilities (e.g. insurance policies in LifeContingencies.jl)

I assume that all of LifeContingencies.jl will become transducers?

  1. Users will have to learn transducers to read your source code and contribute to your projects.
  2. Heavily leaning JuliaActuary in this direction will allow users familiar with the transducers API to have a unified way to use your software.

My takeaway is that you think it is a good way to do things and want people to learn it.

Transducers in lifecontingencies

  1. Because they sell transducers as a parallel programming thing there is this implied desire to parallelize things. And I start to wonder what workload is planned to be so heavy that it is being parallelized?
  2. Models that carry forward an account value will need to use Scan or ScanEmit. Will that be annoying? Is there any desire to model things like universal life in lifecontingencies.jl?
  3. I suppose that aggregate actuarial present values at each timestep is accomplished my mutating some global vector of length timesteps? In NumPy this is done with sum(;axis=dimension). The transducer approach will have better memory complexity in this case, but so would for loops.

I guess aggregating over the time dimension looks like this -

1:3 |> Zip(Map(identity), Map(x -> 10x), Map(x -> 100x)) |> Map(sum) |> collect

Now I'm considering what modeling new business would look like while doing efficient aggregations for each timestep, unsure.

  1. Overall it make me want to do some transducer based implementations of something like the term life benchmark. There is a universal life one as well now.

AccessibleOptimization

  1. I can find AccessibleOptimization.jl on GitLab and see no official docs site, just a site which has a top link explaining how to use Julia packages without reading the docs. It seems like I would have to know the API of the optimization and accessors packages to really appreciate the API. It seems simple enough though, you might want very thorough documentation on your end.

section 6

  1. I do not see the referenced thing in section 6?

When the cashflow depends on a model. An example of this is a floating bond where the coupon paid depends on a view of forward rates. See section 6 on projections for how this is handled.

@alecloudenback
Copy link
Member Author

alecloudenback commented Jul 7, 2023

@MatthewCaseres

Interesting thoughts. It is late so pardon the messy response.

Transducers, Generally

Transducers are a very rich and elegant concept, but the surface exposed here is small:

  • To regular end-users who just use what is given to them here, the transducers internals are effectively completely hidden
  • To moderately advanced users who want to extend the functionality, as the examples show the only real exposure here is a weird function name __foldl__ and a for loop with a weird return signature.

Transducers are more complex conceptually than basic iterators, but I have found them to be vastly superior to iterators. E.g. trying to write how a Composite contract iterator where the two contracts might behave very differently. Or with the iterator interface keeping track of state for a floating rate bond to reference the source projection was complex. It also seems like performance is preserved better for more complex contracts and models.

I think it's a bit of a risk but I think that the flexibility and expressiveness will be worth it. Part of my motivation for the simple examples above is to introduce enough that users aren't scared by it if they want to extend functionality.

Transducers for Liabilities

Earlier versions of LIfeContingencies did use Transducers for, you guessed it, the parts that did not require an exclusive Scan operation. The rest of the package just used arrays passed around. I found that I could improve performance by using generators instead and lazily pass around interim calculations in a Transducers-lite way. However, the generators can have type stability issues (there is no eltype for generators and it requires runtime dispatch). Transducers, along with a custom Scan implementation, should allow me to re-write LIfeContingincies using Transducers. I also want to use the Cashflow data structure because one of the complicated things in that package is handling the timepoints and cashflows- my goal is that this foundation will let me handle that in a simple and flexible way. Additionally, I hope to be able to use the ProjectionKind to flexibly define different outputs one may want (e.g. just cashflows, or a decrement table, or ....)

Misc

I do not see the referenced thing in section 6?

I added it, thanks for catching.

@kasperrisager
Copy link
Contributor

Good to see this moving forward, and to me it looks like the right direction. I'll go by the numbering in the OP.

  1. Couldn't agree more!
  2. I like the way contracts are considered to be composites of more primitive elements.
    a. When it comes to the time dimension, maybe the composites should have time types that are the same or somehow compatible, but maybe it's too early to start constraining that.
    b. How many instruments can actually be handled without projections+models? Only those whose nominal values are known from the outset? If yes, it's worth a consideration to just use projections+models always, since that will be the default for most scenarios, maybe with some syntactic sugar for the simple case.
    c. I haven't tried out Transducers, but it looks good.
  3. Quotes that are combinations of prices and contracts looks like a sound concept. The convenience functions are probably just carried over from the existing Yields.jl, but beware that their naming is a bit backwards now. They are named something with 'yield', but they take yields as inputs and give out quotes. So consider naming them, e.g., quote and put the information about the type of quote in the input types.
  4. So as I understand it, a model really just provides anything that some present_value function needs in order to price a contract. Could be a discount factor, a volatility, anything really.
  5. I really like that fitting a model is just one function, and the input determines what happens in practise. There is one thing that's a little awkward with the version here: fit both takes a model as an input and and gives a model as its output. The input model is sometimes actually a Model, and sometimes not really. Also, the method argument makes sense for some models and not for others. Maybe a more natural interface would be model(model_fit_specification, quotes) where the model_fit_specification would encapsulate what model to fit and how to do it.
  6. (I'll need a bit more time to study this)

Loads of interesting ideas here :-). I'll be back with more later

@alecloudenback
Copy link
Member Author

alecloudenback commented Jul 11, 2023

Thanks for your feedback @kasperrisager !

The convenience functions are probably just carried over from the existing Yields.jl, but beware that their naming is a bit backwards now. They are named something with 'yield', but they take yields as inputs and give out quotes. So consider naming them, e.g., quote and put the information about the type of quote in the input types.

Thanks for the feedback here. I'm open to suggestions, I would just prefer to ensure that things don't get overly verbose. One defense of the current names are that it's all circular: a quote is a price someone was willing to transact at -> implies a valuation model -> calculates a price one would be willing to transact at.

Related: one thing I thought about but did not implement (yet?) was providing a Quote(model,contract) function which would return Quote(present_value(model,contract),contract) (i.e. this would be a present value which also included the contract). This would let users conveniently round-trip but I didn't personally see much use but did seem like it would be satisfyingly complete for a reason I can't fully explain.

I really like that fitting a model is just one function, and the input determines what happens in practise. There is one thing that's a little awkward with the version here: fit both takes a model as an input and and gives a model as its output. The input model is sometimes actually a Model, and sometimes not really. Also, the method argument makes sense for some models and not for others. Maybe a more natural interface would be model(model_fit_specification, quotes) where the model_fit_specification would encapsulate what model to fit and how to do it.

Yes, I agree with your observation that there is some inconsistency:

  • When fitting parameters, currently the first model argument is always a "complete" model, but with default parameters.
  • Differently, the Bootstrap method has Splines as the first argument to fit which are not "complete" models and instead just signal which type of spline to fit.

If I understand correctly, model(model_fit_specification, quotes) would essentially eliminate the fit function? At first glance I actually do kind of like the idea and feels more Julian. Will give this some more thought.

When it comes to the time dimension, maybe the composites should have time types that are the same or somehow compatible, but maybe it's too early to start constraining that.

You mean dates and floating point timepoints? This is another area I have to give some more thought, but with whatever Date convention chosen, don't all calculations map back to some floating point representation of the amount of time from the valuation date? Said another way, there are intermediate steps involved, but Date modeling is floating point timestep modeling with extra steps?

With that point of view, you could have heterogeneously typed timepoint contracts. One may question why one would want the precision of Dates in some part of the model but not others, but I'm not sure there's a reason to limit the compatible types.

How many instruments can actually be handled without projections+models? Only those whose nominal values are known from the outset? If yes, it's worth a consideration to just use projections+models always, since that will be the default for most scenarios, maybe with some syntactic sugar for the simple case.

That's how it's handled under the hood - contracts without any other information needed are wrapped in a Projection with a NullModel.

@kasperrisager
Copy link
Contributor

If I understand correctly, model(model_fit_specification, quotes) would essentially eliminate the fit function?

Yes. But when I come to think about it, there might be a reason to have a fit - in the cases where the model in the input is the starting point for an optimization, it may (or may not) make sense to name it differently. Maybe there's a naming convention from the optimization world that one could follow?

... don't all calculations map back to some floating point representation of the amount of time from the valuation date?

Yes and no. I can see use cases where the cash flows are given at Dates, and where the discounting function takes a Date as input. Then it would be up to the discounting function to decide on a current date and a daycount convention. If, on the other hand, time is given as a Real, it is the cash flow that's deciding the daycount convention, and the current date would be 0.0. Both are fine and should be allowed imo.

I think I was arguing for the requirement that the two different ways were not mixed in the same composite, but you are probably right that it should not be constrained unless absolutely necessary.

@kasperrisager
Copy link
Contributor

Btw, when I try to run this locally, I get the same package resolution error as CI does, do you have a known fix for that?

@alecloudenback
Copy link
Member Author

Btw, when I try to run this locally, I get the same package resolution error as CI does, do you have a known fix for that?

You need to dev ActuaryUtilities, FinanceCore, and Yields and check out the associated PRs (this one, JuliaActuary/ActuaryUtilities.jl#101, and JuliaActuary/FinanceCore.jl#7)

@alecloudenback
Copy link
Member Author

Yes. But when I come to think about it, there might be a reason to have a fit - in the cases where the model in the input is the starting point for an optimization, it may (or may not) make sense to name it differently. Maybe there's a naming convention from the optimization world that one could follow?

It seems like the convention has converged (pun intended) around the solve function, e.g. https://docs.sciml.ai/Optimization/stable/examples/rosenbrock/

@alecloudenback
Copy link
Member Author

I tried to consolidate some of the outstanding questions into the checklist in the OP. Over the coming days I'm going to try to wrap this up and merge the various PRs to the master branches and then in the coming weeks try to synchronize a version bump along with a blog post highlighting major changes and how to migrate if necessary.

@alecloudenback
Copy link
Member Author

I inserted a new section 7 which describes what/why/how for the ProjectionKinds.

@alecloudenback
Copy link
Member Author

Merging this but not releasing yet, let me know if anyone has feedback on the outstanding questions. Check out the new docs (dev, not stable until release is tagged)

Will release after some more review and I get a blog post associated with the ecosystem changes. With the merging of this PR, the master branches of ActuaryUtilities, FinanceCore, and Yields/FinanceModels should all be in sync (still need to be deved since not released)

@alecloudenback alecloudenback marked this pull request as ready for review July 30, 2023 14:21
@alecloudenback alecloudenback merged commit d7549bf into master Jul 30, 2023
6 of 21 checks passed
@alecloudenback alecloudenback deleted the FM-Transducers branch July 30, 2023 14:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

API Update ObservableQuote Questions
3 participants