diff --git a/Project.toml b/Project.toml index 3c64753a..7e3ebe3e 100644 --- a/Project.toml +++ b/Project.toml @@ -1,34 +1,33 @@ -name = "Yields" -uuid = "d7e99b2f-e7f3-4d9e-9f01-2338fc023ad3" +name = "FinanceModels" +uuid = "77f2ae65-bdde-421f-ae9d-22f1af19dd76" authors = ["Alec Loudenback and contributors"] -version = "3.5.0" +version = "4.0.0" [deps] +AccessibleOptimization = "d88a00a0-4a21-4fe4-a515-e2123c37b885" +Accessors = "7d9f7c33-5ae7-4f3b-8dc6-eff91059b697" BSplineKit = "093aae92-e908-43d7-9660-e50ee39d5a0a" +Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" +Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f" FinanceCore = "b9b1ffdd-6612-4b69-8227-7663be06e089" -ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" +IntervalSets = "8197267c-284f-5f27-9208-e0e47529a953" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" -LsqFit = "2fda8390-95c7-5789-9bda-21331edee243" -Optim = "429524aa-4258-5aef-a3af-852621145aeb" -Reexport = "189a3867-3050-52da-a836-e630ba90ab69" -Roots = "f2b01f46-fcfa-551c-844a-d8ac1e96c665" -PrecompileTools = "aea7be01-6a6a-4083-8856-8a6e6704d82a" +Optimization = "7f7a1694-90dd-40f0-9382-eb1efda571ba" +OptimizationMetaheuristics = "3aafef2f-86ae-4776-b337-85a36adf0b55" +OptimizationOptimJL = "36348300-93cb-4f02-beb5-3c3902f8871e" +StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" +Transducers = "28d57a85-8fef-5791-bfe6-a80928e7c999" UnicodePlots = "b8865327-cd53-5732-bb35-84acbb429228" [compat] -BSplineKit = "^0.8" -FinanceCore = "^1" -ForwardDiff = "^0.10" -LsqFit = "^0.12" -Optim = "^1" -Reexport = "^1.2" -Roots = "^1" -PrecompileTools = "^1" -UnicodePlots = "^2" -julia = "^1.6" +julia = "1.6" [extras] +ActuaryUtilities = "bdd23359-8b1c-4f88-b89b-d11982a786f4" +DecFP = "55939f99-70c6-5e9b-8bb0-5071ed7d61fd" +FinanceCore = "b9b1ffdd-6612-4b69-8227-7663be06e089" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" +TestItemRunner = "f8b46487-2199-4994-9208-9a1283c18c0a" [targets] -test = ["Test"] +test = ["Test", "FinanceCore", "ActuaryUtilities", "DecFP", "TestItemRunner"] diff --git a/README.md b/README.md index 51aec07a..b63cbe70 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ -# Yields.jl +# FinanceModels.jl -[![Stable](https://img.shields.io/badge/docs-stable-blue.svg)](https://JuliaActuary.github.io/Yields.jl/stable) -[![Dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://JuliaActuary.github.io/Yields.jl/dev) -[![Build Status](https://github.com/JuliaActuary/Yields.jl/workflows/CI/badge.svg)](https://github.com/JuliaActuary/Yields.jl/actions) -[![Coverage](https://codecov.io/gh/JuliaActuary/Yields.jl/branch/master/graph/badge.svg)](https://codecov.io/gh/JuliaActuary/Yields.jl) +[![Stable](https://img.shields.io/badge/docs-stable-blue.svg)](https://JuliaActuary.github.io/FinanceModels.jl/stable) +[![Dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://JuliaActuary.github.io/FinanceModels.jl/dev) +[![Build Status](https://github.com/JuliaActuary/FinanceModels.jl/workflows/CI/badge.svg)](https://github.com/JuliaActuary/FinanceModels.jl/actions) +[![Coverage](https://codecov.io/gh/JuliaActuary/FinanceModels.jl/branch/master/graph/badge.svg)](https://codecov.io/gh/JuliaActuary/FinanceModels.jl) -**Yields.jl** provides a simple interface for constructing, manipulating, and using yield curves for modeling purposes. +**FinanceModels.jl** provides a simple interface for constructing, manipulating, and using yield curves for modeling purposes. -It's intended to provide common functionality around modeling interest rates, spreads, and miscellaneous yields across the JuliaActuary ecosystem (though not limited to use in JuliaActuary packages). +It's intended to provide common functionality around modeling interest rates, spreads, and miscellaneous FinanceModels across the JuliaActuary ecosystem (though not limited to use in JuliaActuary packages). ![anim_fps2](https://user-images.githubusercontent.com/711879/174458687-860c5d7f-e125-46a9-a706-7d113f1e243b.gif) @@ -15,7 +15,7 @@ It's intended to provide common functionality around modeling interest rates, sp ## QuickStart ```julia -using Yields +using FinanceModels riskfree_maturities = [0.5, 1.0, 1.5, 2.0] riskfree = [5.0, 5.8, 6.4, 6.8] ./ 100 #spot rates, annual effective if unspecified @@ -23,8 +23,8 @@ riskfree = [5.0, 5.8, 6.4, 6.8] ./ 100 #spot rates, annual effective if u spread_maturities = [0.5, 1.0, 1.5, 3.0] # different maturities spread = [1.0, 1.8, 1.4, 1.8] ./ 100 # spot spreads -rf_curve = Yields.Zero(riskfree,riskfree_maturities) -spread_curve = Yields.Zero(spread,spread_maturities) +rf_curve = FinanceModels.Zero(riskfree,riskfree_maturities) +spread_curve = FinanceModels.Zero(spread,spread_maturities) yield = rf_curve + spread_curve # additive combination of the two curves @@ -38,10 +38,10 @@ discount(yield,1.5) # 1 / (1 + 0.064 + 0.014) ^ 1.5 Rates are types that wrap scalar values to provide information about how to determine `discount` and `accumulation` factors. -There are two `CompoundingFrequency` types: +There are two `Frequency` types: -- `Yields.Periodic(m)` for rates that compound `m` times per period (e.g. `m` times per year if working with annual rates). -- `Yields.Continuous()` for continuously compounding rates. +- `FinanceModels.Periodic(m)` for rates that compound `m` times per period (e.g. `m` times per year if working with annual rates). +- `FinanceModels.Continuous()` for continuously compounding rates. #### Examples @@ -64,7 +64,7 @@ Periodic.([0.02,0.03,0.04],2) Continuous.([0.02,0.03,0.04]) ``` -Rates can also be constructed by specifying the `CompoundingFrequency` and then passing a scalar rate: +Rates can also be constructed by specifying the `Frequency` and then passing a scalar rate: ```julia Periodic(1)(0.05) @@ -76,10 +76,10 @@ Continuous()(0.05) Convert rates between different types with `convert`. E.g.: ```julia-repl -r = Rate(Yields.Periodic(12),0.01) # rate that compounds 12 times per rate period (ie monthly) +r = Rate(FinanceModels.Periodic(12),0.01) # rate that compounds 12 times per rate period (ie monthly) -convert(Yields.Periodic(1),r) # convert monthly rate to annual effective -convert(Yields.Continuous(),r) # convert monthly rate to continuous +convert(FinanceModels.Periodic(1),r) # convert monthly rate to annual effective +convert(FinanceModels.Continuous(),r) # convert monthly rate to continuous ``` #### Arithmetic @@ -94,11 +94,11 @@ There are a several ways to construct a yield curve object. If `maturities` is o There is a set of constructor methods which will return a yield curve calibrated to the given inputs. -- `Yields.Zero(rates,maturities)` using a vector of zero rates (sometimes referred to as "spot" rates) -- `Yields.Forward(rates,maturities)` using a vector of forward rates -- `Yields.Par(rates,maturities)` takes a series of yields for securities priced at par. Assumes that maturities <= 1 year do not pay coupons and that after one year, pays coupons with frequency equal to the CompoundingFrequency of the corresponding rate (2 by default). -- `Yields.CMT(rates,maturities)` takes the most commonly presented rate data (e.g. [Treasury.gov](https://www.treasury.gov/resource-center/data-chart-center/interest-rates/Pages/TextView.aspx?data=yield)) and bootstraps the curve given the combination of bills and bonds. -- `Yields.OIS(rates,maturities)` takes the most commonly presented rate data for overnight swaps and bootstraps the curve. Rates assume a single settlement for <1 year and quarterly settlements for 1 year and above. +- `FinanceModels.Zero(rates,maturities)` using a vector of zero rates (sometimes referred to as "spot" rates) +- `FinanceModels.Forward(rates,maturities)` using a vector of forward rates +- `FinanceModels.Par(rates,maturities)` takes a series of FinanceModels for securities priced at par. Assumes that maturities <= 1 year do not pay coupons and that after one year, pays coupons with frequency equal to the Frequency of the corresponding rate (2 by default). +- `FinanceModels.CMT(rates,maturities)` takes the most commonly presented rate data (e.g. [Treasury.gov](https://www.treasury.gov/resource-center/data-chart-center/interest-rates/Pages/TextView.aspx?data=yield)) and bootstraps the curve given the combination of bills and bonds. +- `FinanceModels.OIS(rates,maturities)` takes the most commonly presented rate data for overnight swaps and bootstraps the curve. Rates assume a single settlement for <1 year and quarterly settlements for 1 year and above. ##### Fitting techniques @@ -110,25 +110,25 @@ There are multiple curve fitting methods available: - `NelsonSiegel(τ_initial=1.0)` - `NelsonSiegelSvensson(τ_initial=[1.0,1.0])` -To specify which fitting method to use, pass the object to as the first parameter to the above set of constructors, for example: `Yields.Par(NelsonSiegel(),rates,maturities)`. +To specify which fitting method to use, pass the object to as the first parameter to the above set of constructors, for example: `FinanceModels.Par(NelsonSiegel(),rates,maturities)`. #### Kernel Methods -- `Yields.SmithWilson` curve (used for [discounting in the EU Solvency II framework](https://www.eiopa.europa.eu/sites/default/files/risk_free_interest_rate/12092019-technical_documentation.pdf)) can be constructed either directly by specifying its inner representation or by calibrating to a set of cashflows with known prices. - - These cashflows can conveniently be constructed with a Vector of `Yields.ZeroCouponQuote`s, `Yields.SwapQuote`s, or `Yields.BulletBondQuote`s. +- `FinanceModels.SmithWilson` curve (used for [discounting in the EU Solvency II framework](https://www.eiopa.europa.eu/sites/default/files/risk_free_interest_rate/12092019-technical_documentation.pdf)) can be constructed either directly by specifying its inner representation or by calibrating to a set of cashflows with known prices. + - These cashflows can conveniently be constructed with a Vector of `FinanceModels.ZeroCouponQuote`s, `FinanceModels.SwapQuote`s, or `FinanceModels.BulletBondQuote`s. #### Other Curves -- `Yields.Constant(rate)` takes a single constant rate for all times -- `Yields.Step(rates,maturities)` doesn't interpolate - the rate is flat up to the corresponding time in `times` +- `FinanceModels.Constant(rate)` takes a single constant rate for all times +- `FinanceModels.Step(rates,maturities)` doesn't interpolate - the rate is flat up to the corresponding time in `times` ### Functions -Most of the above yields have the following defined (goal is to have them all): +Most of the above FinanceModels have the following defined (goal is to have them all): - `discount(curve,from,to)` or `discount(curve,to)` gives the discount factor - `accumulation(curve,from,to)` or `accumulation(curve,to)` gives the accumulation factor -- `zero(curve,time)` or `zero(curve,time,CompoundingFrequency)` gives the zero-coupon spot rate for the given time. +- `zero(curve,time)` or `zero(curve,time,Frequency)` gives the zero-coupon spot rate for the given time. - `forward(curve,from,to)` gives the zero rate between the two given times - `par(curve,time)` gives the coupon-paying par equivalent rate for the given time. @@ -136,10 +136,10 @@ Most of the above yields have the following defined (goal is to have them all): Different yield objects can be combined with addition or subtraction. See the [Quickstart](#quickstart) for an example. -When adding a `Yields.AbstractYield` with a scalar or vector, that scalar or vector will be promoted to a yield type via [`Yield()`](#yield). For example: +When adding a `FinanceModels.AbstractYield` with a scalar or vector, that scalar or vector will be promoted to a yield type via [`Yield()`](#yield). For example: ```julia -y1 = Yields.Constant(0.05) +y1 = FinanceModels.Constant(0.05) y2 = y1 + 0.01 # y2 is a yield of 0.06 ``` @@ -150,8 +150,8 @@ Constructed curves can be shifted so that a future timepoint becomes the effecti ```julia-repl julia> zero = [5.0, 5.8, 6.4, 6.8] ./ 100 julia> maturity = [0.5, 1.0, 1.5, 2.0] -julia> curve = Yields.Zero(zero, maturity) -julia> fwd = Yields.ForwardStarting(curve, 1.0) +julia> curve = FinanceModels.Zero(zero, maturity) +julia> fwd = FinanceModels.ForwardStarting(curve, 1.0) julia> discount(curve,1,2) 0.9275624570410582 @@ -162,19 +162,19 @@ julia> discount(fwd,1) # `curve` has effectively been reindexed to `1.0` ## Exported vs Un-exported Functions -Generally, CamelCase methods which construct a datatype are exported as they are unlikely to conflict with other parts of code that may be written. For example, `rate` is un-exported (it must be called with `Yields.rate(...)`) because `rate` is likely a very commonly defined variable within actuarial and financial contexts and there is a high risk of conflicting with defined variables. +Generally, CamelCase methods which construct a datatype are exported as they are unlikely to conflict with other parts of code that may be written. For example, `rate` is un-exported (it must be called with `FinanceModels.rate(...)`) because `rate` is likely a very commonly defined variable within actuarial and financial contexts and there is a high risk of conflicting with defined variables. -Consider using `import Yields` which would require qualifying all methods, but alleviates any namespace conflicts and has the benefit of being explicit about the calls (internally we prefer this in the package design to keep dependencies and their usage clear). +Consider using `import FinanceModels` which would require qualifying all methods, but alleviates any namespace conflicts and has the benefit of being explicit about the calls (internally we prefer this in the package design to keep dependencies and their usage clear). ## Internals -For time-variant yields (ie yield *curves*), the inputs are converted to spot rates and interpolated using quadratic B-splines by default (see documentation for alternatives, such as linear interpolations). +For time-variant FinanceModels (ie yield *curves*), the inputs are converted to spot rates and interpolated using quadratic B-splines by default (see documentation for alternatives, such as linear interpolations). ### Combination Implementation -[Combinations](#combinations) track two different curve objects and are not combined into a single underlying data structure. This means that you may achieve better performance if you combine the rates before constructing a `Yields` representation. The exception to this is `Constant` curves, which *do* get combined into a single structure that is as performant as pre-combined rate structure. +[Combinations](#combinations) track two different curve objects and are not combined into a single underlying data structure. This means that you may achieve better performance if you combine the rates before constructing a `FinanceModels` representation. The exception to this is `Constant` curves, which *do* get combined into a single structure that is as performant as pre-combined rate structure. ## Related Packages - [**`InterestRates.jl`**](https://github.com/felipenoris/InterestRates.jl) specializes in fast rate calculations aimed at valuing fixed income contracts, with business-day-level accuracy. - - Comparative comments: **`Yields.jl`** does not try to provide as precise controls over the timing, structure, and interpolation of the curve. Instead, **`Yields.jl`** provides a minimal, but flexible and intuitive interface for common modeling needs. + - Comparative comments: **`FinanceModels.jl`** does not try to provide as precise controls over the timing, structure, and interpolation of the curve. Instead, **`FinanceModels.jl`** provides a minimal, but flexible and intuitive interface for common modeling needs. diff --git a/docs/Project.toml b/docs/Project.toml index c912c351..023751fa 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,3 +1,3 @@ [deps] Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" -Yields = "d7e99b2f-e7f3-4d9e-9f01-2338fc023ad3" +FinanceModels = "77f2ae65-bdde-421f-ae9d-22f1af19dd76" diff --git a/docs/make.jl b/docs/make.jl index b380ec35..edf1557d 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,14 +1,14 @@ -using Yields +using FinanceModels using Documenter makedocs(; - modules=[Yields], + modules=[FinanceModels], authors="Alec Loudenback and contributors", - repo="https://github.com/JuliaActuary/Yields.jl/blob/{commit}{path}#L{line}", - sitename="Yields.jl", + repo="https://github.com/JuliaActuary/FinanceModels.jl/blob/{commit}{path}#L{line}", + sitename="FinanceModels.jl", format=Documenter.HTML(; prettyurls=get(ENV, "CI", "false") == "true", - canonical="https://JuliaActuary.github.io/Yields.jl", + canonical="https://JuliaActuary.github.io/FinanceModels.jl", assets=String[], ), pages=[ @@ -19,5 +19,5 @@ makedocs(; ) deploydocs(; - repo="github.com/JuliaActuary/Yields.jl", + repo="github.com/JuliaActuary/FinanceModels.jl", ) diff --git a/docs/src/Updates.md b/docs/src/Updates.md new file mode 100644 index 00000000..beb32518 --- /dev/null +++ b/docs/src/Updates.md @@ -0,0 +1,33 @@ +# FinanceModels.jl + +## Design + +- **Contracts** represent insturments that ultimately obligate a payment of cashflows, which may or may not be scenario dependant. +- **Quotes** are observed or reference prices that may be used to `fit` models. +- **Models** are the combination of **assumptions** and **logic** that can then be used to realize the assumed cashflows that arise from a contract. + +## Motivation + +FinanceModels.jl is the evolution of Yields.jl. Yields.jl was originally designed for very nice usage of term structures of yield curves, but three aspects held it back: + +1. The design was very oriented towards interest rates, and it was awkward to stick, e.g. volatility models into a package called Yields.jl +2. The API for contructing curves was inconsistent because there are different ways to construct a given curve and the inputs to constructing a simple bootstrapped curve with a spline through given yields vs a best-fit of a variety of instrumnets was simply a different paradigm. +3. There was a lack of ability to even express some types of contracts that are useful for model-fitting or modeling in general. + +## TODOs +- `bond.frequency.frequency` is awkward +- Core contracts: + - Composite contact (e.g. Fixed + Float -> Swap) + - Forward contact + - Derivatives? + - distinguish between clean and dirty prices +- Projections + - Everythign is currently coerced to a F64/F64 Cashflow, but would like to be flexible with amount and timepoints +- How to integrate Dates? +- Core methods: + - port Yields.jl methods +- Ergonomics: + - +- Package design: + - promote `pv` to FinanceCore given it's utility here + - promote `Cashflow` up to FC diff --git a/docs/src/api.md b/docs/src/api.md index 543315d0..09b2feb4 100644 --- a/docs/src/api.md +++ b/docs/src/api.md @@ -1,6 +1,6 @@ -# Yields API Reference +# FinanceModels API Reference -Please [open an issue](https://github.com/JuliaActuary/Yields.jl/issues) if you encounter any issues or confusion with the package. +Please [open an issue](https://github.com/JuliaActuary/FinanceModels.jl/issues) if you encounter any issues or confusion with the package. ## Rate Types @@ -13,7 +13,7 @@ For example, if we construct a curve like this: rates =[0.01, 0.01, 0.03, 0.05, 0.07, 0.16, 0.35, 0.92, 1.40, 1.74, 2.31, 2.41] ./ 100 mats = [1/12, 2/12, 3/12, 6/12, 1, 2, 3, 5, 7, 10, 20, 30] -curve = Yields.CMT(rates,mats) +curve = FinanceModels.CMT(rates,mats) ``` Then rates from this curve will be typed. For example: @@ -22,7 +22,7 @@ Then rates from this curve will be typed. For example: z = zero(c,10) ``` -Now, `z` will be: `Yields.Rate{Float64, Continuous}(0.01779624378877313, Continuous())` +Now, `z` will be: `FinanceModels.Rate{Float64, Continuous}(0.01779624378877313, Continuous())` This `Rate` has both the rate an the compounding convention embedded in the datatype. @@ -33,11 +33,11 @@ using ActuaryUtilities present_values(z,cashflows) ``` -If you need to extract the rate for some reason, you can get the rate by calling `Yields.rate(...)`. Using the above example, `Yields.rate(z)` will return `0.01779624378877313`. +If you need to extract the rate for some reason, you can get the rate by calling `FinanceModels.rate(...)`. Using the above example, `FinanceModels.rate(z)` will return `0.01779624378877313`. ```@index ``` ```@autodocs -Modules = [Yields] +Modules = [FinanceModels] ``` diff --git a/docs/src/developer.md b/docs/src/developer.md index 8eeb4124..b6bae1ae 100644 --- a/docs/src/developer.md +++ b/docs/src/developer.md @@ -1,27 +1,36 @@ ```@meta -CurrentModule = Yields +CurrentModule = FinanceModels ``` # Developer Notes + ## How are cashflows produced from a contract? + +When we `collect` a contract we get a vector of Cashflows. How does this work under the hood? + +A contract is the logic that defines how cashflow are paid. Sometimes, this requires extra assumptions (e.g. a floating rate bond needs to know what current rates are to determine the payment) and sometimes this does not (a fixed bond effectively has predetermined obligations). + +A combination of a contract and the set of assumptions the define how that contract should behave is contained in a `Projection`. + +** ..... OLD DOCS BELOW ...... ** ## Custom Curve Types -Types that subtype `Yields.AbstractYieldCurve` should implement a few key methods: +Types that subtype `FinanceModels.AbstractYieldCurve` should implement a few key methods: - `discount(curve,to)` should return the discount factor for the given curve through time `to` For example: ```julia -struct MyYield <: Yields.AbstractYieldCurve +struct MyYield <: FinanceModels.AbstractYieldCurve rate end -Yields.discount(c::MyYield,to) = exp(-c.rate * to) +FinanceModels.discount(c::MyYield,to) = exp(-c.rate * to) ``` -By defining the `discount` method as above and subtyping `Yields.AbstractYieldCurve`, Yields.jl has generic functions that will work: +By defining the `discount` method as above and subtyping `FinanceModels.AbstractYieldCurve`, FinanceModels.jl has generic functions that will work: - `zero(curve,to)` returns the zero rate at time `to` - `discount(curve,from,to)` is the discount factor between the two timepoints @@ -34,19 +43,19 @@ If creating a new type of curve, you may find that it's most natural to define o In some contexts, such as creating performant iteration of curves in [EconomicScenarioGenerators.jl](https://github.com/JuliaActuary/EconomicScenarioGenerators.jl), Julia wants to know what type should be expected given an object type. For this reason, we define an internal, un-exported function which returns the `Rate` type expected given a Yield curve. -Sometimes it is most natural or convenient to expect a certain kind of `Rate` from a given curve. In many advanced use-cases (differentiation, stochastic rates), `Continuous` rates are most natural. For this reason, the `DEFAULT_COMPOUNDING` constant within Yields.jl is $(Yields.DEFAULT_COMPOUNDING). Two comments on this: +Sometimes it is most natural or convenient to expect a certain kind of `Rate` from a given curve. In many advanced use-cases (differentiation, stochastic rates), `Continuous` rates are most natural. For this reason, the `DEFAULT_COMPOUNDING` constant within FinanceModels.jl is $(FinanceModels.DEFAULT_COMPOUNDING). Two comments on this: -1. Becuase Yields.jl returns `Rate` types (e.g. `Rate(0.05,Continuous()`) instead of single scalars (e.g. `0.05`) functions within the `JuliaActuary` universe (e.g. `ActuaryUtilities.present_value) know how to treat rates differently and in general users should not ever need to worry about converting between different compounding conventions. +1. Becuase FinanceModels.jl returns `Rate` types (e.g. `Rate(0.05,Continuous()`) instead of single scalars (e.g. `0.05`) functions within the `JuliaActuary` universe (e.g. `ActuaryUtilities.present_value) know how to treat rates differently and in general users should not ever need to worry about converting between different compounding conventions. 2. Developers implementing new `AbstractYieldCurve` types can define their own default. For example, using the `MyYield` example above: - - `__ratetype(::Type{MyYield}) = Yields.Rate{Float64, Continuous}` + - `__ratetype(::Type{MyYield}) = FinanceModels.Rate{Float64, Continuous}` -If the `CompoundingFrequency` is `Continuous`, then it's currently not necessary to define `__ratetype`, as it will fall back onto the generic method defined for `AbstractYieldCurve`s. +If the `Frequency` is `Continuous`, then it's currently not necessary to define `__ratetype`, as it will fall back onto the generic method defined for `AbstractYieldCurve`s. -If the preferred compounding frequency is `Periodic`, then you must either define the methods (`zero`, `forward`,...) for your type or to use the generic methods then you must define `Yields.CompoundingFrequency(curve::MyCurve)` to return the `Periodic` compounding datatype of the rates to return. +If the preferred compounding frequency is `Periodic`, then you must either define the methods (`zero`, `forward`,...) for your type or to use the generic methods then you must define `FinanceModels.Frequency(curve::MyCurve)` to return the `Periodic` compounding datatype of the rates to return. For example, if we wanted `MyCurve` to return `Periodic(1)` rates, then we would define: -`Yields.CompoundingFrequency(curve::MyCurve) = Periodic(1)` +`FinanceModels.Frequency(curve::MyCurve) = Periodic(1)` This latter step is necessary and distinct from `__ratetype`. This is due to `__ratetype` relying on type-only information. The `Periodic` type contains as a datafield the compounding frequency. Therefore, the frequency is not known to the type system and is available only at runtime. diff --git a/docs/src/index.md b/docs/src/index.md index 579ba0c3..94934e67 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -1,21 +1,21 @@ ```@meta -CurrentModule = Yields +CurrentModule = FinanceModels ``` -# Yields.jl +# FinanceModels.jl -[![Stable](https://img.shields.io/badge/docs-stable-blue.svg)](https://JuliaActuary.github.io/Yields.jl/stable) -[![Dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://JuliaActuary.github.io/Yields.jl/dev) -[![Build Status](https://github.com/JuliaActuary/Yields.jl/workflows/CI/badge.svg)](https://github.com/JuliaActuary/Yields.jl/actions) -[![Coverage](https://codecov.io/gh/JuliaActuary/Yields.jl/branch/master/graph/badge.svg)](https://codecov.io/gh/JuliaActuary/Yields.jl) +[![Stable](https://img.shields.io/badge/docs-stable-blue.svg)](https://JuliaActuary.github.io/FinanceModels.jl/stable) +[![Dev](https://img.shields.io/badge/docs-dev-blue.svg)](https://JuliaActuary.github.io/FinanceModels.jl/dev) +[![Build Status](https://github.com/JuliaActuary/FinanceModels.jl/workflows/CI/badge.svg)](https://github.com/JuliaActuary/FinanceModels.jl/actions) +[![Coverage](https://codecov.io/gh/JuliaActuary/FinanceModels.jl/branch/master/graph/badge.svg)](https://codecov.io/gh/JuliaActuary/FinanceModels.jl) -**Yields.jl** provides a simple interface for constructing, manipulating, and using yield curves for modeling purposes. +**FinanceModels.jl** provides a simple interface for constructing, manipulating, and using yield curves for modeling purposes. -It's intended to provide common functionality around modeling interest rates, spreads, and miscellaneous yields across the JuliaActuary ecosystem (though not limited to use in JuliaActuary packages). +It's intended to provide common functionality around modeling interest rates, spreads, and miscellaneous FinanceModels across the JuliaActuary ecosystem (though not limited to use in JuliaActuary packages). ## QuickStart ```julia -using Yields +using FinanceModels riskfree_maturities = [0.5, 1.0, 1.5, 2.0] riskfree = [5.0, 5.8, 6.4, 6.8] ./ 100 #spot rates, annual effective if unspecified @@ -23,8 +23,8 @@ riskfree = [5.0, 5.8, 6.4, 6.8] ./ 100 #spot rates, annual effective if u spread_maturities = [0.5, 1.0, 1.5, 3.0] # different maturities spread = [1.0, 1.8, 1.4, 1.8] ./ 100 # spot spreads -rf_curve = Yields.Zero(riskfree,riskfree_maturities) -spread_curve = Yields.Zero(spread,spread_maturities) +rf_curve = FinanceModels.Zero(riskfree,riskfree_maturities) +spread_curve = FinanceModels.Zero(spread,spread_maturities) yield = rf_curve + spread_curve # additive combination of the two curves @@ -38,10 +38,10 @@ discount(yield,1.5) # 1 / (1 + 0.064 + 0.014) ^ 1.5 Rates are types that wrap scalar values to provide information about how to determine `discount` and `accumulation` factors. -There are two `CompoundingFrequency` types: +There are two `Frequency` types: -- `Yields.Periodic(m)` for rates that compound `m` times per period (e.g. `m` times per year if working with annual rates). -- `Yields.Continuous()` for continuously compounding rates. +- `FinanceModels.Periodic(m)` for rates that compound `m` times per period (e.g. `m` times per year if working with annual rates). +- `FinanceModels.Continuous()` for continuously compounding rates. #### Examples @@ -64,7 +64,7 @@ Periodic.([0.02,0.03,0.04],2) Continuous.([0.02,0.03,0.04]) ``` -Rates can also be constructed by specifying the `CompoundingFrequency` and then passing a scalar rate: +Rates can also be constructed by specifying the `Frequency` and then passing a scalar rate: ```julia Periodic(1)(0.05) @@ -76,10 +76,10 @@ Continuous()(0.05) Convert rates between different types with `convert`. E.g.: ```julia-repl -r = Rate(Yields.Periodic(12),0.01) # rate that compounds 12 times per rate period (ie monthly) +r = Rate(FinanceModels.Periodic(12),0.01) # rate that compounds 12 times per rate period (ie monthly) -convert(Yields.Periodic(1),r) # convert monthly rate to annual effective -convert(Yields.Continuous(),r) # convert monthly rate to continuous +convert(FinanceModels.Periodic(1),r) # convert monthly rate to annual effective +convert(FinanceModels.Continuous(),r) # convert monthly rate to continuous ``` #### Arithmetic @@ -94,11 +94,11 @@ There are a several ways to construct a yield curve object. If `maturities` is o There is a set of constructor methods which will return a yield curve calibrated to the given inputs. -- `Yields.Zero(rates,maturities)` using a vector of zero rates (sometimes referred to as "spot" rates) -- `Yields.Forward(rates,maturities)` using a vector of forward rates -- `Yields.Par(rates,maturities)` takes a series of yields for securities priced at par. Assumes that maturities <= 1 year do not pay coupons and that after one year, pays coupons with frequency equal to the CompoundingFrequency of the corresponding rate (2 by default). -- `Yields.CMT(rates,maturities)` takes the most commonly presented rate data (e.g. [Treasury.gov](https://www.treasury.gov/resource-center/data-chart-center/interest-rates/Pages/TextView.aspx?data=yield)) and bootstraps the curve given the combination of bills and bonds. -- `Yields.OIS(rates,maturities)` takes the most commonly presented rate data for overnight swaps and bootstraps the curve. Rates assume a single settlement for <1 year and quarterly settlements for 1 year and above. +- `FinanceModels.Zero(rates,maturities)` using a vector of zero rates (sometimes referred to as "spot" rates) +- `FinanceModels.Forward(rates,maturities)` using a vector of forward rates +- `FinanceModels.Par(rates,maturities)` takes a series of FinanceModels for securities priced at par. Assumes that maturities <= 1 year do not pay coupons and that after one year, pays coupons with frequency equal to the Frequency of the corresponding rate (2 by default). +- `FinanceModels.CMT(rates,maturities)` takes the most commonly presented rate data (e.g. [Treasury.gov](https://www.treasury.gov/resource-center/data-chart-center/interest-rates/Pages/TextView.aspx?data=yield)) and bootstraps the curve given the combination of bills and bonds. +- `FinanceModels.OIS(rates,maturities)` takes the most commonly presented rate data for overnight swaps and bootstraps the curve. Rates assume a single settlement for <1 year and quarterly settlements for 1 year and above. ##### Fitting techniques @@ -110,25 +110,25 @@ There are multiple curve fitting methods available: - `NelsonSiegel(τ_initial=1.0)` - `NelsonSiegelSvensson(τ_initial=[1.0,1.0])` -To specify which fitting method to use, pass the object to as the first parameter to the above set of constructors, for example: `Yields.Par(NelsonSiegel(),rates,maturities)`. +To specify which fitting method to use, pass the object to as the first parameter to the above set of constructors, for example: `FinanceModels.Par(NelsonSiegel(),rates,maturities)`. #### Kernel Methods -- `Yields.SmithWilson` curve (used for [discounting in the EU Solvency II framework](https://www.eiopa.europa.eu/sites/default/files/risk_free_interest_rate/12092019-technical_documentation.pdf)) can be constructed either directly by specifying its inner representation or by calibrating to a set of cashflows with known prices. - - These cashflows can conveniently be constructed with a Vector of `Yields.ZeroCouponQuote`s, `Yields.SwapQuote`s, or `Yields.BulletBondQuote`s. +- `FinanceModels.SmithWilson` curve (used for [discounting in the EU Solvency II framework](https://www.eiopa.europa.eu/sites/default/files/risk_free_interest_rate/12092019-technical_documentation.pdf)) can be constructed either directly by specifying its inner representation or by calibrating to a set of cashflows with known prices. + - These cashflows can conveniently be constructed with a Vector of `FinanceModels.ZeroCouponQuote`s, `FinanceModels.SwapQuote`s, or `FinanceModels.BulletBondQuote`s. #### Other Curves -- `Yields.Constant(rate)` takes a single constant rate for all times -- `Yields.Step(rates,maturities)` doesn't interpolate - the rate is flat up to the corresponding time in `times` +- `FinanceModels.Constant(rate)` takes a single constant rate for all times +- `FinanceModels.Step(rates,maturities)` doesn't interpolate - the rate is flat up to the corresponding time in `times` ### Functions -Most of the above yields have the following defined (goal is to have them all): +Most of the above FinanceModels have the following defined (goal is to have them all): - `discount(curve,from,to)` or `discount(curve,to)` gives the discount factor - `accumulation(curve,from,to)` or `accumulation(curve,to)` gives the accumulation factor -- `zero(curve,time)` or `zero(curve,time,CompoundingFrequency)` gives the zero-coupon spot rate for the given time. +- `zero(curve,time)` or `zero(curve,time,Frequency)` gives the zero-coupon spot rate for the given time. - `forward(curve,from,to)` gives the zero rate between the two given times - `par(curve,time)` gives the coupon-paying par equivalent rate for the given time. @@ -136,10 +136,10 @@ Most of the above yields have the following defined (goal is to have them all): Different yield objects can be combined with addition or subtraction. See the [Quickstart](#quickstart) for an example. -When adding a `Yields.AbstractYield` with a scalar or vector, that scalar or vector will be promoted to a yield type via [`Yield()`](#yield). For example: +When adding a `FinanceModels.AbstractYield` with a scalar or vector, that scalar or vector will be promoted to a yield type via [`Yield()`](#yield). For example: ```julia -y1 = Yields.Constant(0.05) +y1 = FinanceModels.Constant(0.05) y2 = y1 + 0.01 # y2 is a yield of 0.06 ``` @@ -150,8 +150,8 @@ Constructed curves can be shifted so that a future timepoint becomes the effecti ```julia-repl julia> zero = [5.0, 5.8, 6.4, 6.8] ./ 100 julia> maturity = [0.5, 1.0, 1.5, 2.0] -julia> curve = Yields.Zero(zero, maturity) -julia> fwd = Yields.ForwardStarting(curve, 1.0) +julia> curve = FinanceModels.Zero(zero, maturity) +julia> fwd = FinanceModels.ForwardStarting(curve, 1.0) julia> discount(curve,1,2) 0.9275624570410582 @@ -162,19 +162,19 @@ julia> discount(fwd,1) # `curve` has effectively been reindexed to `1.0` ## Exported vs Un-exported Functions -Generally, CamelCase methods which construct a datatype are exported as they are unlikely to conflict with other parts of code that may be written. For example, `rate` is un-exported (it must be called with `Yields.rate(...)`) because `rate` is likely a very commonly defined variable within actuarial and financial contexts and there is a high risk of conflicting with defined variables. +Generally, CamelCase methods which construct a datatype are exported as they are unlikely to conflict with other parts of code that may be written. For example, `rate` is un-exported (it must be called with `FinanceModels.rate(...)`) because `rate` is likely a very commonly defined variable within actuarial and financial contexts and there is a high risk of conflicting with defined variables. -Consider using `import Yields` which would require qualifying all methods, but alleviates any namespace conflicts and has the benefit of being explicit about the calls (internally we prefer this in the package design to keep dependencies and their usage clear). +Consider using `import FinanceModels` which would require qualifying all methods, but alleviates any namespace conflicts and has the benefit of being explicit about the calls (internally we prefer this in the package design to keep dependencies and their usage clear). ## Internals -For time-variant yields (ie yield *curves*), the inputs are converted to spot rates and interpolated using quadratic B-splines by default (see documentation for alternatives, such as linear interpolations). +For time-variant FinanceModels (ie yield *curves*), the inputs are converted to spot rates and interpolated using quadratic B-splines by default (see documentation for alternatives, such as linear interpolations). ### Combination Implementation -[Combinations](#combinations) track two different curve objects and are not combined into a single underlying data structure. This means that you may achieve better performance if you combine the rates before constructing a `Yields` representation. The exception to this is `Constant` curves, which *do* get combined into a single structure that is as performant as pre-combined rate structure. +[Combinations](#combinations) track two different curve objects and are not combined into a single underlying data structure. This means that you may achieve better performance if you combine the rates before constructing a `FinanceModels` representation. The exception to this is `Constant` curves, which *do* get combined into a single structure that is as performant as pre-combined rate structure. ## Related Packages - [**`InterestRates.jl`**](https://github.com/felipenoris/InterestRates.jl) specializes in fast rate calculations aimed at valuing fixed income contracts, with business-day-level accuracy. - - Comparative comments: **`Yields.jl`** does not try to provide as precise controls over the timing, structure, and interpolation of the curve. Instead, **`Yields.jl`** provides a minimal, but flexible and intuitive interface for common modeling needs. + - Comparative comments: **`FinanceModels.jl`** does not try to provide as precise controls over the timing, structure, and interpolation of the curve. Instead, **`FinanceModels.jl`** provides a minimal, but flexible and intuitive interface for common modeling needs. diff --git a/src/AbstractYieldCurve.jl b/src/AbstractYieldCurve.jl deleted file mode 100644 index 9dc2f926..00000000 --- a/src/AbstractYieldCurve.jl +++ /dev/null @@ -1,16 +0,0 @@ -abstract type AbstractYieldCurve <: FinanceCore.AbstractYield end - -""" -A `YieldCurveFitParameters` is a structure which contains associated parameters for a yield curve fitting procedure. The type of the object determines the method, and the values of the object determine the parameters. - -If the fitting data and the rates are passed as `<:Real` numbers instead of a type of `Rate`s, the default interpretation may vary depending on the fitting type/parameter. See the individual docstrings of the types for more information. - -Available types are: - -- [`Bootstrap`](@ref) -- [`NelsonSiegel`](@ref) -- [`NelsonSiegelSvensson`](@ref) -""" -abstract type YieldCurveFitParameters end - -Base.Broadcast.broadcastable(x::T) where {T<:YieldCurveFitParameters} = Ref(x) \ No newline at end of file diff --git a/src/Contract.jl b/src/Contract.jl new file mode 100644 index 00000000..222a01d1 --- /dev/null +++ b/src/Contract.jl @@ -0,0 +1,228 @@ +# Extending the Core contracts from FinanceCore + +### Bonds +module Bond +import ..FinanceCore: Cashflow, Quote, AbstractContract, maturity, Timepoint +using ..FinanceCore + +using FinanceCore: Periodic, Continuous, Rate + +export ZCBYield, ZCBPrice, ParSwapYield, ParYield, CMTYield + +abstract type AbstractBond <: AbstractContract end +maturity(b::AbstractBond) = b.maturity + + +""" +ZCBPrice(discount,maturity) +ZCBPrice(yield::Vector) + +Takes discount factors. + +Use broadcasting to create a set of quotes given a collection of prices and maturities, e.g. `ZCBPrice.(FinanceModels,maturities)`. +""" +ZCBPrice(price, time) = Quote(price, Cashflow(1.0, time)) + + +""" +ZCBYield(yield,maturity) +ZCBYield(yield::Vector) + +Takes zero (sometimes called "spot") rates. Assumes annual effective compounding (`Periodic(1)``) unless given a `Rate` with a different compounding frequency. + +Use broadcasting to create a set of quotes given a collection of FinanceModels and maturities, e.g. `ZCBYield.(FinanceModels,maturities)`. +""" +ZCBYield(yield, time) = Quote(discount(yield, time), Cashflow(1.0, time)) + +struct Fixed{F<:FinanceCore.Frequency,N<:Real,M<:Timepoint} <: AbstractBond + coupon_rate::N # coupon_rate / frequency is the actual payment amount + frequency::F + maturity::M +end + +function Base.isapprox(a::Fixed, b::Fixed) + isapprox(a.coupon_rate, b.coupon_rate) && ==(a.frequency, b.frequency) && isapprox(a.maturity, b.maturity) +end + +# function timesteps(b::AbstractBond) +# f = 1 / b.frequency.frequency +# f:f:b.maturity +# end + + +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 + +__coerce_periodic(y::Periodic) = y +__coerce_periodic(y::T) where {T<:Int} = Periodic(y) + +""" +ParYield(yield,maturity) +ParYield(yield::Vector) + +Takes bond equivalent FinanceModels, and assumes that instruments <= one year maturity pay no coupons and that the rest pay semi-annual. Alternative, you may pass a `Rate` as the yield and the coupon frequency will be inferred from the `Rate`'s frequency. + +Use broadcasting to create a set of quotes given a collection of FinanceModels and maturities, e.g. `ParYield.(FinanceModels,maturities)`. +""" +function ParYield(yield, maturity; frequency=Periodic(2)) + # assume the frequency is two or infer it from the yield + frequency = __coerce_periodic(frequency) + price = 1.0 # by definition for a par bond + coupon_rate = rate(frequency(yield)) + return Quote(price, Fixed(coupon_rate, frequency, maturity)) +end +function ParYield(yield::Rate{N,T}, maturity; frequency=Periodic(2)) where {T<:Periodic,N} + frequency = yield.compounding + price = 1.0 # by definition for a par bond + coupon_rate = rate(frequency(yield)) + return Quote(price, Fixed(coupon_rate, frequency, maturity)) +end + +# the fixed leg of the swap +function ParSwapYield(yield, maturity; frequency=Periodic(4)) + frequency = __coerce_periodic(frequency) + ParYield(yield, maturity; frequency=frequency) +end + +""" +CMTYield(yield,maturity) +CMTYield(yield::Vector) +Takes constant maturity (treasury) FinanceModels (bond equivalent), and assumes that instruments <= one year maturity pay no coupons and that the rest pay semi-annual. + +Use broadcasting to create a set of quotes given a collection of FinanceModels and maturities, e.g. `CMTYield.(FinanceModels,maturities)`. +""" +function CMTYield(yield, maturity) + # Assume maturity < 1 don't pay coupons and are therefore discount bonds + # Assume maturity > 1 pay coupons and are therefore par bonds + frequency = Periodic(2) + r, v = if maturity ≤ 1 + Periodic(0.0, 1), discount(yield, maturity) + else + # coupon paying par bond + frequency(yield), 1.0 + end + return Quote(v, Fixed(rate(r), r.compounding, maturity)) +end + +""" +OISYield(yield [, maturity=eachindex(yield)])) + +Assumes that maturities less than or equal to 12 months are settled once (per Hull textbook, 4.7), otherwise quarterly and that the FinanceModels given are bond equivalent. + +Use broadcasting to create a set of quotes given a collection of FinanceModels and maturities, e.g. `OISYield.(FinanceModels,maturities)`. + +""" +function OISYield(yield, maturity=eachindex(yield)) + + if maturity <= 1 + return Quote(discount(yield, maturity), Fixed(0.0, Periodic(1), maturity)) + else + frequency = Periodic(4) + r = frequency(yield) + return Quote(1.0, Fixed(rate(r), frequency, maturity)) + end +end + +""" +ForwardYields(yields,times) +Returns a vector of `Quote` corresponding to the . + +# Examples +```julia-repl +julia> FinanceModels.Bond.ForwardYields([0.01,0.02],[1.,3.]) +2-element Vector{Quote{Float64, Cashflow{Float64, Float64}}}: + Quote{Float64, Cashflow{Float64, Float64}}(0.9900990099009901, Cashflow{Float64, Float64}(1.0, 1.0)) + Quote{Float64, Cashflow{Float64, Float64}}(0.9423223345470445, Cashflow{Float64, Float64}(1.0, 3.0)) +``` +""" +function ForwardYields(yields, times=eachindex(yields)) + df = 1.0 + t_prior = 0.0 + map(zip(yields, times)) do (y, t) + df *= discount(y, t - t_prior) + t_prior = t + Quote( + df, + Cashflow(1.0, t) + ) + end +end + + +# Bond utility funcs + +function coupon_times(maturity, frequency) + Δt = min(1 / frequency, maturity) + times = maturity:-Δt:0 + if iszero(last(times)) + return reverse(times[1:end-1]) + else + return reverse(times) + end +end +coupon_times(b::AbstractBond) = coupon_times(b.maturity, b.frequency.frequency) + + +for op = (:ZCBPrice, :ZCBYield, :ParYield, :ParSwapYield, :CMTYield, :ForwardYield) + eval(quote + $op(x::Vector; kwargs...) = $op.(x, eachindex(x); kwargs...) + end) +end + + +end + +struct CommonEquity <: FinanceCore.AbstractContract end + +module Option +import ..FinanceCore: AbstractContract, Timepoint + +struct EuroCall{S<:AbstractContract,K<:Real,M<:Timepoint} <: AbstractContract + underlying::S + strike::K + maturity::M +end +end + +""" +Forward(time,instrument) + +The instrument is relative to the Forward time. +e.g. if you have a `Forward(1.0, Cashflow(1.0, 3.0))` then the instrument is a cashflow that pays 1.0 at time 4.0 +""" +struct Forward{T<:FinanceCore.Timepoint,I<:FinanceCore.AbstractContract} <: FinanceCore.AbstractContract + time::T + instrument::I +end + + +# create a matrix of cashflows and a vector of timepoints +# timepoints need not be spaced evenly +function cashflows_timepoints(qs) + cfs = map(q -> collect(q), qs) + times = map(cfs) do cf + map(c -> c.time, cf) + end |> Iterators.flatten |> unique |> sort! + + m = zeros(length(times), length(qs)) + + for t in 1:length(times) + for q in 1:length(qs) + for c in 1:length(cfs[q]) + if times[t] == cfs[q][c].time + m[t, q] += cfs[q][c].amount + end + end + end + end + m + return m, times +end + +function cashflows_timepoints(qs::Vector{Q}) where {Q<:Quote} + cashflows_timepoints([q.instrument for q in qs]) +end \ No newline at end of file diff --git a/src/FinanceModels.jl b/src/FinanceModels.jl new file mode 100644 index 00000000..cc20b6fc --- /dev/null +++ b/src/FinanceModels.jl @@ -0,0 +1,45 @@ +module FinanceModels + +import Dates +using FinanceCore +using FinanceCore: present_value, discount, accumulation +using OptimizationOptimJL +using OptimizationMetaheuristics +using StaticArrays +using IntervalSets +using AccessibleOptimization +using Accessors +using LinearAlgebra +using Transducers +import BSplineKit +import UnicodePlots +using Transducers: @next, complete, __foldl__, asfoldable +import Distributions + + + +include("utils.jl") +include("Contract.jl") +include("model/Model.jl") +include("Projection.jl") +include("Fit.jl") + +export Cashflow, Quote, Forward, CommonEquity, Option + +using .Bond: ZCBYield, ZCBPrice, ParSwapYield, ParYield, CMTYield, ForwardYields, OISYield +export Bond, ZCBYield, ZCBPrice, ParSwapYield, ParYield, CMTYield, ForwardYields, OISYield + +export Spline + +export NullModel, Yield, discount, accumulation, zero, forward + +using .Yield: par +export par + +export Equity, Volatility +export Projection, CashflowProjection +export pv +export Fit, fit + + +end diff --git a/src/NelsonSiegelSvensson.jl b/src/NelsonSiegelSvensson.jl deleted file mode 100644 index 1f756d37..00000000 --- a/src/NelsonSiegelSvensson.jl +++ /dev/null @@ -1,239 +0,0 @@ -abstract type ParametricModel <: AbstractYieldCurve end -Base.Broadcast.broadcastable(x::T) where {T<:ParametricModel} = Ref(x) - - -""" - NelsonSiegel(rates::AbstractVector, maturities::AbstractVector; τ_initial=1.0) - -Return the NelsonSiegel fitted parameters. The rates should be zero spot rates. If `rates` are not `Rate`s, then they will be interpreted as `Continuous` `Rate`s. - - NelsonSiegel(β₀, β₁, β₂, τ₁) - -Parameters of Nelson and Siegel (1987) parametric model: - -- β₀ represents a long-term interest rate -- β₁ represents a time-decay component -- β₂ represents a hump -- τ₁ controls the location of the hump - -# Examples - -```julia-repl -julia> β₀, β₁, β₂, τ₁ = 0.6, -1.2, -1.9, 3.0 -julia> nsm = Yields.NelsonSiegel.(β₀, β₁, β₂, τ₁) - -# Extend Help - -## References -- https://onriskandreturn.com/2019/12/01/nelson-siegel-yield-curve-model/ -- https://www.bis.org/publ/bppdf/bispap25.pdf - -``` -""" -struct NelsonSiegelCurve{T} <: ParametricModel - β₀::T - β₁::T - β₂::T - τ₁::T - - function NelsonSiegelCurve(β₀::T, β₁::T, β₂::T, τ₁::T) where {T<:Real} - (τ₁ <= 0) && throw(DomainError("Wrong parameter ranges")) - return new{T}(β₀, β₁, β₂, τ₁) - end -end -__ratetype(::Type{NelsonSiegelCurve{T}}) where {T}= Yields.Rate{T, typeof(DEFAULT_COMPOUNDING)} - - -""" - NelsonSiegel(τ_initial) - NelsonSiegel() # defaults to τ_initial=1.0 - -This parameter set is used to fit the Nelson-Siegel parametric model to given rates. `τ_initial` should be a scalar and is used as the starting τ value in the optimization. The default value for `τ_initial` is 1.0. - -When fitting rates using this `YieldCurveFitParameters` object, the Nelson-Siegel model is used. If constructing curves and the rates are not `Rate`s (ie you pass a `Vector{Float64}`), then they will be interpreted as `Continuous` `Rate`s. - -See for more: - -- [`Zero`](@ref) -- [`Forward`](@ref) -- [`Par`](@ref) -- [`CMT`](@ref) -- [`OIS`](@ref) -""" -struct NelsonSiegel{T} <: YieldCurveFitParameters - τ_initial::T -end -NelsonSiegel() = NelsonSiegel(1.0) - -function Base.zero(ns::NelsonSiegelCurve, t) - if iszero(t) - # zero rate is undefined for t = 0 - t += eps() - end - Continuous.(ns.β₀ .+ ns.β₁ .* (1.0 .- exp.(-t ./ ns.τ₁)) ./ (t ./ ns.τ₁) .+ ns.β₂ .* ((1.0 .- exp.(-t ./ ns.τ₁)) ./ (t ./ ns.τ₁) .- exp.(-t ./ ns.τ₁))) -end -FinanceCore.discount(ns::NelsonSiegelCurve, t) = discount.(zero.(ns,t),t) - - -function fit_β(ns::NelsonSiegel,func,yields,maturities,τ) - Δₘ = vcat([maturities[1]], diff(maturities)) - param₀ = [1.0, 0.0, 0.0] - _rate(m, p) = rate.(func.(NelsonSiegelCurve(p[1], p[2], p[3],only(τ)), m)) - - return LsqFit.curve_fit(_rate, maturities, yields, Δₘ,param₀) -end - -function __fit_NS(ns::NelsonSiegel,func,yields,maturities,τ) - f(τ) = β_sum_sq_resid(ns,func,yields,maturities,τ) - r = Optim.optimize(f, [ns.τ_initial]) - - τ = only(Optim.minimizer(r)) - - return τ, fit_β(ns,func,yields,maturities,τ) -end - -function β_sum_sq_resid(ns,func,yields,maturities,τ) - result = fit_β(ns,func,yields,maturities,τ) - return sum(r^2 for r in result.resid) -end - -function Zero(ns::NelsonSiegel,yields, maturities=eachindex(yields)) - yields = rate.(Continuous().(yields)) - func = zero - τ, result = __fit_NS(ns,func,yields,maturities,ns.τ_initial) - return NelsonSiegelCurve(result.param[1], result.param[2], result.param[3], τ) -end - -function Par(ns::NelsonSiegel,yields, maturities=eachindex(yields)) - yields = rate.(Continuous().(yields)) - func = par - τ, result = __fit_NS(ns,func,yields,maturities,ns.τ_initial) - return NelsonSiegelCurve(result.param[1], result.param[2], result.param[3], τ) -end - -function Forward(ns::NelsonSiegel,yields, maturities=eachindex(yields)) - yields = rate.(Continuous().(yields)) - func = forward - τ, result = __fit_NS(ns,func,yields,maturities,ns.τ_initial) - return NelsonSiegelCurve(result.param[1], result.param[2], result.param[3], τ) -end - - -""" - NelsonSiegelSvensson(yields::AbstractVector, maturities::AbstractVector; τ_initial=[1.0,1.0]) - -Return the NelsonSiegelSvensson fitted parameters. The rates should be continuous zero spot rates. If `rates` are not `Rate`s, then they will be interpreted as `Continuous` `Rate`s. - -When fitting rates using this `YieldCurveFitParameters` object, the Nelson-Siegel model is used. If constructing curves and the rates are not `Rate`s (ie you pass a `Vector{Float64}`), then they will be interpreted as `Continuous` `Rate`s. - -See for more: - - - [`Zero`](@ref) - - [`Forward`](@ref) - - [`Par`](@ref) - - [`CMT`](@ref) - - [`OIS`](@ref) - - NelsonSiegelSvensson(β₀, β₁, β₂, β₃, τ₁, τ₂) - -Parameters of Svensson (1994) parametric model: - -- β₀ represents a long-term interest rate -- β₁ represents a time-decay component -- β₂ represents a hump -- β₃ represents a second hum -- τ₁ controls the location of the hump -- τ₁ controls the location of the second hump - - -# Examples - -```julia-repl -julia> β₀, β₁, β₂, β₃, τ₁, τ₂ = 0.6, -1.2, -2.1, 3.0, 1.5 -julia> nssm = NelsonSiegelSvensson.NelsonSiegelSvensson.(β₀, β₁, β₂, β₃, τ₁, τ₂) - -## References -- https://onriskandreturn.com/2019/12/01/nelson-siegel-yield-curve-model/ -- https://www.bis.org/publ/bppdf/bispap25.pdf - -``` -""" -struct NelsonSiegelSvenssonCurve{T} <: ParametricModel - β₀::T - β₁::T - β₂::T - β₃::T - τ₁::T - τ₂::T - - function NelsonSiegelSvenssonCurve(β₀::T, β₁::T, β₂::T, β₃::T, τ₁::T, τ₂::T) where {T<:Real} - (τ₁ <= 0 || τ₂ <= 0) && throw(DomainError("Wrong parameter ranges")) - return new{T}(β₀, β₁, β₂, β₃, τ₁, τ₂) - end -end -__ratetype(::Type{NelsonSiegelSvenssonCurve{T}}) where {T}= Yields.Rate{T, typeof(DEFAULT_COMPOUNDING)} -""" - NelsonSiegelSvensson(τ_initial) - NelsonSiegelSvensson() # defaults to τ_initial=[1.0,1.0] - -This parameter set is used to fit the Nelson-Siegel parametric model to given rates. `τ_initial` should be a two element vector and is used as the starting τ value in the optimization. The default value for `τ_initial` is [1.0,1.0]. - -See for more: - -- [`Zero`](@ref) -- [`Forward`](@ref) -- [`Par`](@ref) -- [`CMT`](@ref) -- [`OIS`](@ref) -""" -struct NelsonSiegelSvensson{T} <: YieldCurveFitParameters - τ_initial::T -end -NelsonSiegelSvensson() = NelsonSiegelSvensson([1.0,1.0]) - -function fit_β(ns::NelsonSiegelSvensson,func,yields,maturities,τ) - Δₘ = vcat([maturities[1]], diff(maturities)) - param₀ = [1.0, 0.0, 0.0, 0.0] - _rate(m, p) = rate.(func.(NelsonSiegelSvenssonCurve(p[1], p[2], p[3],p[4],first(τ),last(τ)), m)) - return LsqFit.curve_fit(_rate, maturities, yields, Δₘ,param₀) -end - -function __fit_NS(ns::NelsonSiegelSvensson,func,yields,maturities,τ) - f(τ) = β_sum_sq_resid(ns,func,yields,maturities,τ) - r = Optim.optimize(f, ns.τ_initial) - - τ = Optim.minimizer(r)[[1,2]] - - return τ, fit_β(ns,func,yields,maturities,τ) -end - - -function Zero(ns::NelsonSiegelSvensson,yields, maturities=eachindex(yields)) - yields = rate.(Continuous().(yields)) - func = zero - τ, result = __fit_NS(ns,func,yields,maturities,ns.τ_initial) - return NelsonSiegelSvenssonCurve(result.param[1], result.param[2], result.param[3],result.param[4], first(τ), last(τ)) -end - -function Par(ns::NelsonSiegelSvensson,yields, maturities=eachindex(yields)) - yields = rate.(Continuous().(yields)) - func = par - τ, result = __fit_NS(ns,func,yields,maturities,ns.τ_initial) - return NelsonSiegelSvenssonCurve(result.param[1], result.param[2], result.param[3],result.param[4], first(τ), last(τ)) -end - -function Forward(ns::NelsonSiegelSvensson,yields, maturities=eachindex(yields)) - yields = rate.(Continuous().(yields)) - func = forward - τ, result = __fit_NS(ns,func,yields,maturities,ns.τ_initial) - return NelsonSiegelSvenssonCurve(result.param[1], result.param[2], result.param[3],result.param[4], first(τ), last(τ)) -end - -function Base.zero(nss::NelsonSiegelSvenssonCurve, t) - if iszero(t) - # zero rate is undefined for t = 0 - t += eps() - end - Continuous.(nss.β₀ .+ nss.β₁ .* (1.0 .- exp.(-t ./ nss.τ₁)) ./ (t ./ nss.τ₁) .+ nss.β₂ .* ((1.0 .- exp.(-t ./ nss.τ₁)) ./ (t ./ nss.τ₁) .- exp.(-t ./ nss.τ₁)) .+ nss.β₃ .* ((1.0 .- exp.(-t ./ nss.τ₂)) ./ (t ./ nss.τ₂) .- exp.(-t ./ nss.τ₂))) -end -FinanceCore.discount(nss::NelsonSiegelSvenssonCurve, t) = discount.(zero.(nss,t),t) diff --git a/src/Projection.jl b/src/Projection.jl new file mode 100644 index 00000000..4f25bf79 --- /dev/null +++ b/src/Projection.jl @@ -0,0 +1,123 @@ + +abstract type AbstractProjection end + +struct Projection{C,M,K} <: AbstractProjection + contract::C + model::M + kind::K +end + +## ProjectionKind ############################### +# controls what gets produced from the model, +# e.g. if you just want cashflows or you want full amortization schedule you might define an AmortizationSchedule kind which shows principle, interest, etc. + +abstract type ProjectionKind end + +struct CashflowProjection <: ProjectionKind end + +# Collecting a Projection ####################### +# Map(identity) is a Transducer, for which `collect` is defined. More on Transducers below + +# collecting a Projection gives your the reducable defined below with __foldl__ +Base.collect(p::P) where {P<:AbstractProjection} = p |> Map(identity) |> collect +# collecting a contract wraps the contract in with the default Proejction, defined next +Base.collect(c::C) where {C<:FinanceCore.AbstractContract} = Projection(c) |> Map(identity) |> collect + +# Default Projections ########################## + +# the default projection is just one where we get the cashflows and assume that the contract needs +# no assumptions/model to determine the cashflows (the contract will error if a certain model is needed) +Projection(c) = Projection(c, NullModel(), CashflowProjection()) +# if the model is also given, assume that we want a `CashflowProjection` by default +Projection(c, m) = Projection(c, m, CashflowProjection()) + + +# Reducibles ################################### + +# a more composible, efficient way to create a collection of things that you can apply subsequent transformations to +# (and those transformations can be Transducers). +# https://juliafolds2.github.io/Transducers.jl/stable/howto/reducibles/ +# https://www.youtube.com/watch?v=6mTbuzafcII + + +# There are two ways to define a reducible collection provided by Transducers.jl: +# `asfoldable` where you can define your reducible in terms of transducers +# `__foldl__` where you can define the collection using a `for` loop +# and `foldl__` you can also define state that is used within the loop + +# this wraps a contract in a default proejction and makes a contract a reducible collection of cashflows +function Transducers.asfoldable(c::C) where {C<:FinanceCore.AbstractContract} + Projection(c) |> Map(identity) +end + +# A cashflow is the simplest, single item reducible collection +@inline function Transducers.__foldl__(rf, val, p::Projection{C,M,K}) where {C<:Cashflow,M,K} + for i in 1:1 + val = @next(rf, val, p.contract) + end + return complete(rf, val) +end + +# +@inline function Transducers.__foldl__(rf, val, p::Projection{C,M,K}) where {C<:Bond.Fixed,M,K} + b = p.contract + ts = Bond.coupon_times(b) + coup = b.coupon_rate / b.frequency.frequency + for t in ts + 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 + + +# here a floating bond references the projections's model to determine +# what the refernece rate is at that point in time +@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 + +# we simply concatenate two reducible collections to create a composite contract +@inline function Transducers.asfoldable(p::Projection{C,M,K}) where {C<:FinanceCore.Composite,M,K} + # creates two sub-projections where the contract projected is decomposed to a non-composite contract + # and then conctanate the two projections together + ap = @set p.contract = p.contract.a + bp = @set p.contract = p.contract.b + (ap, bp) |> Cat() +end + +# forward contract defines a set of cashflows that are relative to a future point in time, +# so we adjust the resulting cashflows `time`s by the forward start date +@inline function Transducers.asfoldable(p::Projection{C,M,K}) where {C<:Forward,M,K<:CashflowProjection} + fwd_start = p.contract.time + p_alt = @set p.contract = p.contract.instrument + p_alt |> Map(cf -> @set cf.time += fwd_start) +end + +@inline function Transducers.asfoldable(p::Projection{C,M,K}) where {C<:Cashflow,M,K<:CashflowProjection} + Ref(p.contract) |> Map(identity) +end \ No newline at end of file diff --git a/src/RateCombination.jl b/src/RateCombination.jl deleted file mode 100644 index b5eb964e..00000000 --- a/src/RateCombination.jl +++ /dev/null @@ -1,166 +0,0 @@ -## Curve Manipulations -""" - RateCombination(curve1,curve2,operation) - -Creates a datastructure that will perform the given `operation` after independently calculating the effects of the two curves. -Can only be created via the public API by using the `+`, `-`, `*`, and `/` operatations on `AbstractYield` objects. - -As this is double the normal operations when performing calculations, if you are using the curve in performance critical locations, you should consider transforming the inputs and -constructing a single curve object ahead of time. -""" -struct RateCombination{T,U,V} <: AbstractYieldCurve - r1::T - r2::U - op::V -end -__ratetype(::Type{RateCombination{T,U,V}}) where {T,U,V}= __ratetype(T) - -FinanceCore.rate(rc::RateCombination, time) = rc.op(rate(rc.r1, time), rate(rc.r2, time)) -function FinanceCore.discount(rc::RateCombination, time) - a1 = discount(rc.r1, time)^(-1 / time) - 1 - a2 = discount(rc.r2, time)^(-1 / time) - 1 - return 1 / (1 + rc.op(a1, a2))^time -end - -Base.zero(rc::RateCombination, time) = zero(rc,time,Periodic(1)) -function Base.zero(rc::RateCombination, time, cf::C) where {C<:FinanceCore.CompoundingFrequency} - d = discount(rc,time) - i = Periodic(1/d^(1/time)-1,1) - return convert(cf, i) # c.zero is a curve of continuous rates represented as floats. explicitly wrap in continuous before converting -end - -""" - Yields.AbstractYieldCurve + Yields.AbstractYieldCurve - -The addition of two yields will create a `RateCombination`. For `rate`, `discount`, and `accumulation` purposes the spot rates of the two curves will be added together. -""" -function Base.:+(a::AbstractYieldCurve, b::AbstractYieldCurve) - return RateCombination(a, b, +) -end - -function Base.:+(a::Constant, b::Constant) - a_kind = rate(a).compounding - rate_new_basis = rate(convert(a_kind, rate(b))) - return Constant( - Rate( - rate(a.rate) + rate_new_basis, - a_kind - ) - ) -end - -function Base.:+(a::T, b) where {T<:AbstractYieldCurve} - return a + Constant(b) -end - -function Base.:+(a, b::T) where {T<:AbstractYieldCurve} - return Constant(a) + b -end - -""" - Yields.AbstractYieldCurve * Yields.AbstractYieldCurve - -The multiplication of two yields will create a `RateCombination`. For `rate`, `discount`, and `accumulation` purposes the spot rates of the two curves will be added together. This can be useful, for example, if you wanted to after-tax a yield. - -# Examples - -```julia-repl -julia> m = Yields.Constant(0.01) * 0.79; - -julia> accumulation(m,1) -1.0079 - -julia> accumulation(.01*.79,1) -1.0079 -``` -""" -function Base.:*(a::AbstractYieldCurve, b::AbstractYieldCurve) - return RateCombination(a, b, *) -end - -function Base.:*(a::Constant, b::Constant) - a_kind = rate(a).compounding - rate_new_basis = rate(convert(a_kind, rate(b))) - return Constant( - Rate( - rate(a.rate) * rate_new_basis, - a_kind - ) - ) -end - -function Base.:*(a::T, b) where {T<:AbstractYieldCurve} - return a * Constant(b) -end - -function Base.:*(a, b::T) where {T<:AbstractYieldCurve} - return Constant(a) * b -end - -""" - Yields.AbstractYieldCurve - Yields.AbstractYieldCurve - -The subtraction of two yields will create a `RateCombination`. For `rate`, `discount`, and `accumulation` purposes the spot rates of the second curves will be subtracted from the first. -""" -function Base.:-(a::AbstractYieldCurve, b::AbstractYieldCurve) - return RateCombination(a, b, -) -end - -function Base.:-(a::Constant, b::Constant) - a_kind = rate(a).compounding - rate_new_basis = rate(convert(a_kind, rate(b))) - return Constant( - Rate( - rate(a.rate) - rate_new_basis, - a_kind - ) - ) -end - -function Base.:-(a::T, b) where {T<:AbstractYieldCurve} - return a - Constant(b) -end - -function Base.:-(a, b::T) where {T<:AbstractYieldCurve} - return Constant(a) - b -end - -""" - Yields.AbstractYieldCurve / Yields.AbstractYieldCurve - -The division of two yields will create a `RateCombination`. For `rate`, `discount`, and `accumulation` purposes the spot rates of the two curves will have the first divided by the second. This can be useful, for example, if you wanted to gross-up a yield to be pre-tax. - -# Examples - -```julia-repl -julia> m = Yields.Constant(0.01) / 0.79; - -julia> accumulation(d,1) -1.0126582278481013 - -julia> accumulation(.01/.79,1) -1.0126582278481013 -``` -""" -function Base.:/(a::AbstractYieldCurve, b::AbstractYieldCurve) - return RateCombination(a, b, /) -end - -function Base.:/(a::Constant, b::Constant) - a_kind = rate(a).compounding - rate_new_basis = rate(convert(a_kind, rate(b))) - return Constant( - Rate( - rate(a.rate) / rate_new_basis, - a_kind - ) - ) -end - -function Base.:/(a::T, b) where {T<:AbstractYieldCurve} - return a / Constant(b) -end - -function Base.:/(a, b::T) where {T<:AbstractYieldCurve} - return Constant(a) / b -end \ No newline at end of file diff --git a/src/SmithWilson.jl b/src/SmithWilson.jl deleted file mode 100644 index 9aed5048..00000000 --- a/src/SmithWilson.jl +++ /dev/null @@ -1,224 +0,0 @@ -abstract type ObservableQuote end - -""" - ZeroCouponQuote(price, maturity) - -Quote for a set of zero coupon bonds with given `price` and `maturity`. - -# Examples - -```julia-repl -julia> prices = [1.3, 0.1, 4.5] -julia> maturities = [1.2, 2.5, 3.6] -julia> swq = Yields.ZeroCouponQuote.(prices, maturities) -``` -""" -struct ZeroCouponQuote <: ObservableQuote - price - maturity -end - -""" - SwapQuote(yield, maturity, frequency) - -Quote for a set of interest rate swaps with the given `yield` and `maturity` and a given payment `frequency`. - -# Examples - -```julia-repl -julia> maturities = [1.2, 2.5, 3.6] -julia> interests = [-0.02, 0.3, 0.04] -julia> prices = [1.3, 0.1, 4.5] -julia> frequencies = [2,1,2] -julia> swq = Yields.SwapQuote.(interests, maturities, frequencies) -``` -""" -struct SwapQuote <: ObservableQuote - yield - maturity - frequency - function SwapQuote(yield, maturity, frequency) - frequency <= 0 && throw(DomainError("Payment frequency must be positive")) - return new(yield, maturity, frequency) - end -end - - -""" - BulletBondQuote(yield, price, maturity, frequency) - -Quote for a set of fixed interest bullet bonds with given `yield`, `price`, `maturity` and a given payment frequency `frequency`. - -Construct a vector of quotes for use with SmithWilson methods, e.g. by broadcasting over an array of inputs. - -# Examples - -```julia-repl -julia> maturities = [1.2, 2.5, 3.6] -julia> interests = [-0.02, 0.3, 0.04] -julia> prices = [1.3, 0.1, 4.5] -julia> frequencies = [2,1,2] -julia> bbq = Yields.BulletBondQuote.(interests, maturities, prices, frequencies) -``` -""" -struct BulletBondQuote <: ObservableQuote - yield - price - maturity - frequency - - function BulletBondQuote(yield, maturity, price, frequency) - frequency <= 0 && throw(DomainError("Payment frequency must be positive")) - return new(yield, maturity, price, frequency) - end -end - - -""" - SmithWilson(zcq::Vector{ZeroCouponQuote}; ufr, α) - SmithWilson(swq::Vector{SwapQuote}; ufr, α) - SmithWilson(bbq::Vector{BulletBondQuote}; ufr, α) - SmithWilson(times<:AbstractVector, cashflows<:AbstractMatrix, prices<:AbstractVector; ufr, α) - SmithWilson(u, qb; ufr, α) - -Create a yield curve object that implements the Smith-Wilson interpolation/extrapolation scheme. - -Positional arguments to construct a curve: - -- Quoted instrument as the first argument: either a `Vector` of `ZeroCouponQuote`s, `SwapQuote`s, or `BulletBondQuote`s, or -- A set of `times`, `cashflows`, and `prices`, or -- A curve can be with `u` is the timepoints coming from the calibration, and `qb` is the internal parameterization of the curve that ensures that the calibration is correct. Users may prefer the other constructors but this mathematical constructor is also available. - -Required keyword arguments: - -- `ufr` is the Ultimate Forward Rate, the forward interest rate to which the yield curve tends, in continuous compounding convention. -- `α` is the parameter that governs the speed of convergence towards the Ultimate Forward Rate. It can be typed with `\\alpha[TAB]` -""" -struct SmithWilson{TU<:AbstractVector,TQb<:AbstractVector} <: AbstractYieldCurve - u::TU - qb::TQb - ufr - α - - # Inner constructor ensures that vector lengths match - function SmithWilson{TU,TQb}(u, qb; ufr, α) where {TU<:AbstractVector,TQb<:AbstractVector} - if length(u) != length(qb) - throw(DomainError("Vectors u and qb in SmithWilson must have equal length")) - end - return new(u, qb, ufr, α) - end -end - -SmithWilson(u::TU, qb::TQb; ufr, α) where {TU<:AbstractVector,TQb<:AbstractVector} = SmithWilson{TU,TQb}(u, qb; ufr = ufr, α = α) - -__ratetype(::Type{SmithWilson{TU,TQb}}) where {TU,TQb}= Yields.Rate{Float64, Yields.Continuous} - -""" - H_ordered(α, t_min, t_max) - -The Smith-Wilson H function with ordered arguments (for better performance than using min and max). -""" -function H_ordered(α, t_min, t_max) - return α * t_min + exp(-α * t_max) * sinh(-α * t_min) -end - -""" - H(α, t1, t2) - -The Smith-Wilson H function implemented in a faster way. -""" -function H(α, t1::T, t2::T) where {T} - return t1 < t2 ? H_ordered(α, t1, t2) : H_ordered(α, t2, t1) -end - -H(α, t1, t2) = H(α, promote(t1, t2)...) - -H(α, t1vec::AbstractVector, t2) = [H(α, t1, t2) for t1 in t1vec] -H(α, t1vec::AbstractVector, t2vec::AbstractVector) = [H(α, t1, t2) for t1 in t1vec, t2 in t2vec] -# This can be optimized by going to H_ordered directly, but it might be a bit cumbersome -H(α, tvec::AbstractVector) = H(α, tvec, tvec) - - -FinanceCore.discount(sw::SmithWilson, t) = exp(-sw.ufr * t) * (1.0 + H(sw.α, sw.u, t) ⋅ sw.qb) -Base.zero(sw::SmithWilson, t) = Continuous(sw.ufr - log(1.0 + H(sw.α, sw.u, t) ⋅ sw.qb) / t) -Base.zero(sw::SmithWilson, t, cf::FinanceCore.CompoundingFrequency) = convert(cf, zero(sw, t)) - -function SmithWilson(times::AbstractVector, cashflows::AbstractMatrix, prices::AbstractVector; ufr, α) - Q = Diagonal(exp.(-ufr * times)) * cashflows - q = vec(sum(Q, dims = 1)) # We want q to be a column vector - QHQ = Q' * H(α, times) * Q - b = QHQ \ (prices - q) - Qb = Q * b - return SmithWilson(times, Qb; ufr = ufr, α = α) -end - -""" - timepoints(zcq::Vector{ZeroCouponQuote}) - timepoints(bbq::Vector{BulletBondQuote}) - -Return the times associated with the `cashflows` of the instruments. -""" -function timepoints(qs::Vector{Q}) where {Q<:ObservableQuote} - frequency = maximum(q.frequency for q in qs) - timestep = 1 / frequency - maturity = maximum(q.maturity for q in qs) - return [timestep:timestep:maturity...] -end - - -""" - cashflows(interests, maturities, frequency) - timepoints(zcq::Vector{ZeroCouponQuote}) - timepoints(bbq::Vector{BulletBondQuote}) - -Produce a cash flow matrix for a set of instruments with given `interests` and `maturities` -and a given payment frequency `frequency`. All instruments are assumed to have their first payment at time 1/`frequency` -and have their last payment at the largest multiple of 1/`frequency` less than or equal to the input maturity. -""" -function cashflows(interests, maturities, frequencies) - frequency = lcm(frequencies) - fq = inv.(frequencies) - timestep = 1 / frequency - floored_mats = floor.(maturities ./ timestep) .* timestep - times = timestep:timestep:maximum(floored_mats) - # we need to determine the coupons in relation to the payment date, not time zero - time_adj = floored_mats .% fq - - cashflows = [ - # if on a coupon date and less than maturity, pay coupon - ((((t + time_adj[instrument]) % fq[instrument] ≈ 0) && t <= floored_mats[instrument]) ? interests[instrument] / frequencies[instrument] : 0.0) + - (t ≈ floored_mats[instrument] ? 1.0 : 0.0) # add maturity payment - for t in times, instrument = eachindex(interests) - ] - - return cashflows -end - -function cashflows(qs::Vector{Q}) where {Q<:ObservableQuote} - yield = [q.yield for q in qs] - maturity = [q.maturity for q in qs] - frequency = [q.frequency for q in qs] - return cashflows(yield, maturity, frequency) -end - -# Utility methods for calibrating Smith-Wilson directly from quotes -function SmithWilson(zcq::Vector{ZeroCouponQuote}; ufr, α) - n = length(zcq) - maturities = [q.maturity for q in zcq] - prices = [q.price for q in zcq] - return SmithWilson(maturities, Matrix{Float64}(I, n, n), prices; ufr = ufr, α = α) -end - -function SmithWilson(swq::Vector{SwapQuote}; ufr, α) - times = timepoints(swq) - cfs = cashflows(swq) - ones(length(swq)) - return SmithWilson(times, cfs, ones(length(swq)), ufr = ufr, α = α) -end - -function SmithWilson(bbq::Vector{BulletBondQuote}; ufr, α) - times = timepoints(bbq) - cfs = cashflows(bbq) - prices = [q.price for q in bbq] - return SmithWilson(times, cfs, prices, ufr = ufr, α = α) -end \ No newline at end of file diff --git a/src/Yields.jl b/src/Yields.jl deleted file mode 100644 index c870eef0..00000000 --- a/src/Yields.jl +++ /dev/null @@ -1,45 +0,0 @@ -module Yields - -using Reexport -using PrecompileTools -using FinanceCore -using FinanceCore: Rate, rate, discount, accumulation, Periodic, Continuous, forward -@reexport using FinanceCore: Rate, rate, discount, accumulation, Periodic, Continuous, forward -import BSplineKit -import ForwardDiff -using LinearAlgebra -using UnicodePlots -import LsqFit -import Optim - -# don't export type, as the API of Yields.Zero is nicer and -# less polluting than Zero and less/equally verbose as ZeroYieldCurve or ZeroCurve -export LinearSpline, QuadraticSpline, - Bootstrap, NelsonSiegel, NelsonSiegelSvensson, SmithWilson - -const DEFAULT_COMPOUNDING = Yields.Continuous() - -include("AbstractYieldCurve.jl") - -include("utils.jl") -include("bootstrap.jl") -include("SmithWilson.jl") -include("generics.jl") -include("RateCombination.jl") -include("NelsonSiegelSvensson.jl") - -include("precompiles.jl") - - -function MethodError_hint(io::IO, ex::InexactError) - hint = "\nA Periodic rate requires also passing a compounding frequency." * - "\nFor example, call Periodic($(ex.val), 2) for a rate compounded twice per period." - print(io, hint) -end - -function __init__() - Base.Experimental.register_error_hint(MethodError_hint, MethodError) - nothing -end - -end diff --git a/src/bootstrap.jl b/src/bootstrap.jl deleted file mode 100644 index 1878895a..00000000 --- a/src/bootstrap.jl +++ /dev/null @@ -1,279 +0,0 @@ -# bootstrapped class of curve methods -""" - Boostrap(interpolation_method=QuadraticSpline) - -This `YieldCurveFitParameters` object defines the interpolation method to use when bootstrapping the curve. Provided options are `QuadraticSpline()` (the default) and `LinearSpline()`. You may also pass a custom interpolation method with the function signature of `f(xs, ys) -> f(x) -> y`. - -If constructing curves and the rates are not `Rate`s (ie you pass a `Vector{Float64}`), then they will be interpreted as `Periodic(1)` `Rate`s, except the [`Par`](@ref) curve, which is interpreted as `Periodic(2)` `Rate`s. [`CMT`](@ref) and [`OIS`](@ref) FinanceCore.CompoundingFrequency assumption depends on the corresponding maturity. - -See for more: - -- [`Zero`](@ref) -- [`Forward`](@ref) -- [`Par`](@ref) -- [`CMT`](@ref) -- [`OIS`](@ref) -""" -struct Bootstrap{T} <: YieldCurveFitParameters - interpolation::T -end -__default_rate_interpretation(ns,r::T) where {T<:Rate} = r -__default_rate_interpretation(::Type{Bootstrap{T}},r::U) where {T,U<:Real} = Periodic(r,1) - -function Bootstrap() - return Bootstrap(QuadraticSpline()) -end - -struct BootstrapCurve{T,U,V} <: AbstractYieldCurve - rates::T - maturities::U - zero::V # function time -> continuous zero rate -end -FinanceCore.discount(yc::T, time) where {T<:BootstrapCurve} = exp(-yc.zero(time) * time) - -__ratetype(::Type{BootstrapCurve{T,U,V}}) where {T,U,V}= Yields.Rate{Float64, typeof(DEFAULT_COMPOUNDING)} - -# Forward curves - -""" - ForwardStarting(curve,forwardstart) - -Rebase a `curve` so that `discount`/`accumulation`/etc. are re-based so that time zero from the new curves perspective is the given `forwardstart` time. - -# Examples - -```julia-repl -julia> zero = [5.0, 5.8, 6.4, 6.8] ./ 100 -julia> maturity = [0.5, 1.0, 1.5, 2.0] -julia> curve = Yields.Zero(zero, maturity) -julia> fwd = Yields.ForwardStarting(curve, 1.0) - -julia> FinanceCore.discount(curve,1,2) -0.9275624570410582 - -julia> FinanceCore.discount(fwd,1) # `curve` has effectively been reindexed to `1.0` -0.9275624570410582 -``` - -# Extended Help - -While `ForwardStarting` could be nested so that, e.g. the third period's curve is the one-period forward of the second period's curve, it will be more efficient to reuse the initial curve from a runtime and compiler perspective. - -`ForwardStarting` is not used to construct a curve based on forward rates. See [`Forward`](@ref) instead. -""" -struct ForwardStarting{T,U} <: AbstractYieldCurve - curve::U - forwardstart::T -end -__ratetype(::Type{ForwardStarting{T,U}}) where {T,U}= __ratetype(U) - -function FinanceCore.discount(c::ForwardStarting, to) - FinanceCore.discount(c.curve, c.forwardstart, to + c.forwardstart) -end - -function Base.zero(c::ForwardStarting, to,cf::C) where {C<:FinanceCore.CompoundingFrequency} - z = forward(c.curve,c.forwardstart,to+c.forwardstart) - return convert(cf,z) -end - - -""" - Constant(rate::Real, cf::CompoundingFrequency=Periodic(1)) - Constant(r::Rate) - -Construct a yield object where the spot rate is constant for all maturities. If `rate` is not a `Rate` type, will assume `Periodic(1)` for the compounding frequency - -# Examples - -```julia-repl -julia> y = Yields.Constant(0.05) -julia> FinanceCore.discount(y,2) -0.9070294784580498 # 1 / (1.05) ^ 2 -``` -""" -struct Constant{T} <: AbstractYieldCurve - rate::T - Constant(rate::T) where {T<:Rate} = new{T}(rate) -end - -__ratetype(::Type{Constant{T}}) where {T} = T -__default_rate_interpretation(::Type{Constant},r) = Periodic(r,1) -FinanceCore.CompoundingFrequency(c::Constant{T}) where {T} = c.rate.compounding - -function Constant(rate::T) where {T<:Real} - r = __default_rate_interpretation(Constant,rate) - return Constant(r) -end - -Base.zero(c::Constant, time) = c.rate -Base.zero(c::Constant, time, cf::FinanceCore.CompoundingFrequency) = convert(cf, c.rate) -FinanceCore.rate(c::Constant) = c.rate -FinanceCore.rate(c::Constant, time) = c.rate -FinanceCore.discount(r::Constant, time) = FinanceCore.discount(r.rate, time) -FinanceCore.discount(r::Constant, from, to) = FinanceCore.discount(r.rate, to - from) -FinanceCore.accumulation(r::Constant, time) = accumulation(r.rate, time) -FinanceCore.accumulation(r::Constant, from, to) = accumulation(r.rate, to - from) - -""" - Step(rates,times) - -Create a yield curve object where the applicable rate is the effective rate of interest applicable until corresponding time. If `rates` is not a `Vector{Rate}`, will assume `Periodic(1)` type. - -The last rate will be applied to any time after the last time in `times`. - -# Examples - -```julia-repl -julia>y = Yields.Step([0.02,0.05], [1,2]) - -julia>rate(y,0.5) -0.02 - -julia>rate(y,1.5) -0.05 - -julia>rate(y,2.5) -0.05 -``` -""" -struct Step{R,T} <: AbstractYieldCurve - rates::R - times::T - function Step(rates,times=eachindex(rates)) - r = __default_rate_interpretation.(Step,rates) - new{typeof(r),typeof(times)}(r, times) - end -end -__ratetype(::Type{Step{R,T}}) where {R,T}= eltype(R) -__default_rate_interpretation(::Type{Step},r) = Periodic(r,1) -FinanceCore.CompoundingFrequency(c::Step{T}) where {T} = first(c.rates).compounding - - - -function FinanceCore.discount(y::Step, time) - v = 1.0 - last_time = 0.0 - - - for (rate,t) in zip(y.rates,y.times) - duration = min(time - last_time,t-last_time) - v *= FinanceCore.discount(rate,duration) - last_time = t - (last_time > time) && return v - - end - - # if we did not return in the loop, then we extend the last rate - v *= FinanceCore.discount(last(y.rates), time - last_time) - return v -end - - - -function Zero(b::Bootstrap,rates, maturities) - rates = __default_rate_interpretation.(typeof(b),rates) - return _zero_inner(rates,maturities,b.interpolation) -end - -# zero is different than the other boostrapped curves in that it doesn't actually need to bootstrap -# because the rate are already zero rates. Instead, we just cut straight to the -# appropriate interpolation function based on the type dispatch. -function _zero_inner(rates, maturities, interp::QuadraticSpline) - continuous_zeros = rate.(Continuous.(rates)) - return BootstrapCurve( - rates, - maturities, - cubic_interp([0.0; maturities],[first(continuous_zeros); continuous_zeros]) - ) -end - -function _zero_inner(rates, maturities, interp::LinearSpline) - continuous_zeros = rate.(Continuous.(rates)) - return BootstrapCurve( - rates, - maturities, - linear_interp([0.0; maturities],[first(continuous_zeros); continuous_zeros]) - ) -end - -# fallback for user provided interpolation function -function _zero_inner(rates, maturities, interp::T) where {T} - continuous_zeros = rate.(Continuous.(rates)) - return BootstrapCurve( - rates, - maturities, - interp([0.0; maturities],[first(continuous_zeros); continuous_zeros]) - ) -end - -function Par(b::Bootstrap,rates, maturities) - rates = __coerce_rate.(rates,Periodic(2)) - return BootstrapCurve( - rates, - maturities, - # assume that maturities less than or equal to 12 months are settled once, otherwise semi-annual - # per Hull 4.7 - bootstrap(rates, maturities, [m <= 1 ? nothing : 1 / r.compounding.frequency for (r, m) in zip(rates, maturities)], b.interpolation) - ) -end - -function Forward(b::Bootstrap,rates, maturities) - rates = __default_rate_interpretation.(typeof(b),rates) - # convert to zeros and pass to Zero - disc_v = Vector{Float64}(undef, length(rates)) - - v = 1.0 - - for (i,r) = enumerate(rates) - Δt = maturities[i] - (i == 1 ? 0 : maturities[i-1]) - v *= FinanceCore.discount(r, Δt) - disc_v[i] = v - end - - z = (1.0 ./ disc_v) .^ (1 ./ maturities) .- 1 # convert disc_v to zero - return Zero(b,z, maturities) -end - -function CMT(b::Bootstrap,rates, maturities) - rs = map(zip(rates, maturities)) do (r, m) - if m <= 1 - Rate(r, Periodic(1 / m)) - else - Rate(r, Periodic(2)) - end - end - - CMT(b,rs, maturities) -end - -function CMT(b::Bootstrap,rates::Vector{T}, maturities) where {T<:Rate} - return BootstrapCurve( - rates, - maturities, - # assume that maturities less than or equal to 12 months are settled once, otherwise semi-annual - # per Hull 4.7 - bootstrap(rates, maturities, [m <= 1 ? nothing : 0.5 for m in maturities], b.interpolation) - ) -end - - -function OIS(b::Bootstrap,rates, maturities) - rs = map(zip(rates, maturities)) do (r, m) - if m <= 1 - Rate(r, Periodic(1 / m)) - else - Rate(r, Periodic(4)) - end - end - - return OIS(b,rs, maturities) -end -function OIS(b::Bootstrap,rates::Vector{<:Rate}, maturities) - return BootstrapCurve( - rates, - maturities, - # assume that maturities less than or equal to 12 months are settled once, otherwise quarterly - # per Hull 4.7 - bootstrap(rates, maturities, [m <= 1 ? nothing : 1 / 4 for m in maturities], b.interpolation) - ) -end diff --git a/src/fit.jl b/src/fit.jl new file mode 100644 index 00000000..6b3c9286 --- /dev/null +++ b/src/fit.jl @@ -0,0 +1,94 @@ +module Fit + +abstract type FitMethod end + +struct Loss{T} <: FitMethod + fn::T +end + +struct Bootstrap <: FitMethod + # spline method +end + + +end + + +__default_optic(m::Yield.Constant) = OptArgs(@optic(_.rate.value) => -1.0 .. 1.0) +__default_optic(m::Yield.IntermediateYieldCurve) = OptArgs(@optic(_.ys[end]) => 0.0 .. 1.0) +__default_optic(m::Yield.NelsonSiegel) = OptArgs([ + @optic(_.τ₁) => 0.0 .. 100.0 + @optic(_.β₀) => -10.0 .. 10.0 + @optic(_.β₁) => -10.0 .. 10.0 + @optic(_.β₂) => -10.0 .. 10.0 +]...) +__default_optic(m::Yield.NelsonSiegelSvensson) = OptArgs([ + @optic(_.τ₁) => 0.0 .. 100.0, + @optic(_.τ₂) => 0.0 .. 100.0, + @optic(_.β₀) => -10.0 .. 10.0, + @optic(_.β₁) => -10.0 .. 10.0, + @optic(_.β₂) => -10.0 .. 10.0, + @optic(_.β₃) => -10.0 .. 10.0, +]...) +__default_optic(m::Equity.BlackScholesMerton) = __default_optic(m.σ) +__default_optic(m::Volatility.Constant) = OptArgs(@optic(_.σ) => -0.0 .. 10.0) + + +__default_optim(m) = ECA() + +function fit(mod0, quotes, method::F=Fit.Loss(x -> x^2); + variables=__default_optic(mod0), + optimizer=__default_optim(mod0) +) where +{F<:Fit.Loss} + # find the rate that minimizes the loss function w.r.t. the calculated price vs the quotes + f = __loss_single_function(method, quotes) + # some solvers want a `Vector` instead of `SVector` + ops = OptProblemSpec(f, SVector, mod0, variables) + sol = solve(ops, optimizer) + return sol.uobj + +end + +function fit(mod0::Spline.BSpline, quotes, method::Fit.Bootstrap) + discount_vector = [0.0] + times = [maturity(quotes[1])] + + discount_vector[1] = let + m = fit(Yield.Constant(), [quotes[1]], Fit.Loss(x -> x^2)) + discount(m, times[1]) + end + + for i in eachindex(quotes)[2:end] + q = quotes[i] + push!(times, maturity(q)) + push!(discount_vector, 0.0) + m = Yield.IntermediateYieldCurve(mod0, times, discount_vector) + discount_vector[i] = let + m = fit(m, [q], Fit.Loss(x -> x^2)) + discount(m, times[i]) + end + + end + zero_vec = -log.(clamp.(discount_vector, 0.00001, 1)) ./ times + return Yield.Spline(mod0, [zero(eltype(times)); times], [first(zero_vec); zero_vec]) + # return Yield.Spline(mod0, times, zero_vec) + +end + +function fit(mod0::Yield.SmithWilson, quotes) + cm, ts = cashflows_timepoints(quotes) + prices = [q.price for q in quotes] + + return Yield.SmithWilson(ts, cm, prices; ufr=mod0.ufr, α=mod0.α) + +end + +function __loss_single_function(loss_method, quotes) + function loss(m, quotes) + return mapreduce(+, quotes) do q + loss_method.fn(present_value(m, q.instrument) - q.price) + end + end + return Base.Fix2(loss, quotes) # a function that takes a model and returns the loss +end \ No newline at end of file diff --git a/src/generics.jl b/src/generics.jl deleted file mode 100644 index 0d9cf763..00000000 --- a/src/generics.jl +++ /dev/null @@ -1,263 +0,0 @@ - -## Generic and Fallbacks -""" - discount(yc, to) - discount(yc, from,to) - -The discount factor for the yield curve `yc` for times `from` through `to`. -""" -FinanceCore.discount(yc::T, from, to) where {T<:AbstractYieldCurve}= discount(yc, to) / discount(yc, from) - -""" - forward(yc, from, to, CompoundingFrequency=Periodic(1)) - -The forward `Rate` implied by the yield curve `yc` between times `from` and `to`. -""" -function FinanceCore.forward(yc::T, from, to) where {T<:AbstractYieldCurve} - return forward(yc, from, to, DEFAULT_COMPOUNDING) -end - -function FinanceCore.forward(yc::T, from, to, cf::FinanceCore.CompoundingFrequency) where {T<:AbstractYieldCurve} - r = Periodic((accumulation(yc, to) / accumulation(yc, from))^(1 / (to - from)) - 1, 1) - return convert(cf, r) -end - -function FinanceCore.forward(yc::T, from) where {T<:AbstractYieldCurve} - to = from + 1 - return forward(yc, from, to) -end - -function FinanceCore.CompoundingFrequency(curve::T) where {T<:AbstractYieldCurve} - return DEFAULT_COMPOUNDING -end - - -""" - par(curve,time;frequency=2) - -Calculate the par yield for maturity `time` for the given `curve` and `frequency`. Returns a `Rate` object with periodicity corresponding to the `frequency`. The exception to this is if `time` is less than what the payments allowed by frequency (e.g. a time `0.5` but with frequency `1`) will effectively assume frequency equal to 1 over `time`. - -# Examples - -```julia-repl -julia> c = Yields.Constant(0.04); - -julia> Yields.par(c,4) -Yields.Rate{Float64, Yields.Periodic}(0.03960780543711406, Yields.Periodic(2)) - -julia> Yields.par(c,4;frequency=1) -Yields.Rate{Float64, Yields.Periodic}(0.040000000000000036, Yields.Periodic(1)) - -julia> Yields.par(c,0.6;frequency=4) -Yields.Rate{Float64, Yields.Periodic}(0.039413626195875295, Yields.Periodic(4)) - -julia> Yields.par(c,0.2;frequency=4) -Yields.Rate{Float64, Yields.Periodic}(0.039374942589460726, Yields.Periodic(5)) - -julia> Yields.par(c,2.5) -Yields.Rate{Float64, Yields.Periodic}(0.03960780543711406, Yields.Periodic(2)) -``` -""" -function par(curve, time; frequency=2) - mat_disc = discount(curve, time) - coup_times = coupon_times(time,frequency) - coupon_pv = sum(discount(curve,t) for t in coup_times) - Δt = step(coup_times) - r = (1-mat_disc) / coupon_pv - cfs = [t == last(coup_times) ? 1+r : r for t in coup_times] - # `sign(r)`` is used instead of `1` because there are times when the coupons are negative so we want to flip the sign - cfs = [-1;cfs] - r = internal_rate_of_return(cfs,[0;coup_times]) - frequency_inner = min(1/Δt,max(1 / Δt, frequency)) - r = convert(Periodic(frequency_inner),r) - return r -end - -""" - zero(curve,time) - zero(curve,time,CompoundingFrequency) - -Return the zero rate for the curve at the given time. -""" -function Base.zero(c::YC, time) where {YC<:AbstractYieldCurve} - zero(c, time, FinanceCore.CompoundingFrequency(c)) -end - -function Base.zero(c::YC, time, cf::C) where {YC<:AbstractYieldCurve,C<:FinanceCore.CompoundingFrequency} - df = discount(c, time) - r = -log(df)/time - return convert(cf, Continuous(r)) # c.zero is a curve of continuous rates represented as floats. explicitly wrap in continuous before converting -end - -""" - accumulation(yc, from, to) - -The accumulation factor for the yield curve `yc` for times `from` through `to`. -""" -function FinanceCore.accumulation(yc::AbstractYieldCurve, time) - return 1 ./ discount(yc, time) -end - -function FinanceCore.accumulation(yc::AbstractYieldCurve, from, to) - return 1 ./ discount(yc, from, to) -end - - -""" - Par(rates, maturities=eachindex(rates) - Par(p::YieldCurveFitParameters, rates, maturities=eachindex(rates) - -Construct a curve given a set of bond equivalent yields and the corresponding maturities. Assumes that maturities <= 1 year do not pay coupons and that after one year, pays coupons with frequency equal to the CompoundingFrequency of the corresponding rate (normally the default for a `Rate` is `1`, but when constructed via `Par` the default compounding Frequency is `2`). - -See [`bootstrap`](@ref) for more on the `interpolation` parameter, which is set to `QuadraticSpline()` by default. - -# Examples - -```julia-repl - -julia> par = [6.,8.,9.5,10.5,11.0,11.25,11.38,11.44,11.48,11.5] ./ 100 -julia> maturities = [t for t in 1:10] -julia> curve = Par(par,maturities); -julia> zero(curve,1) -Rate(0.06000000000000005, Periodic(1)) - -``` -""" -function Yields.Par(rates,maturities=eachindex(rates)) - # bump to a constant yield if only given one rate - length(rates) == 1 && return Constant(first(rates)) - return Yields.Par(Bootstrap(),rates,maturities) -end - - -""" - Forward(rates,maturities=eachindex(rates)) - Forward(p::YieldCurveFitParameters, rates,maturities=eachindex(rates)) - -Takes a vector of 1-period forward rates and constructs a discount curve. The method of fitting the curve to the data is determined by the [`YieldCurveFitParameters`](@ref) object `p`, which is a `Boostrap(QuadraticSpline())` by default. - -If `rates` is a vector of floating point number instead of a vector `Rate`s, see the [`YieldCurveFitParameters`](@ref) for how the rate will be interpreted. - -# Examples - -```julia-repl -julia> Yields.Forward( [0.01,0.02,0.03] ); - -julia> Yields.Forward( Yields.Continuous.([0.01,0.02,0.03]) ); - -``` -""" -function Yields.Forward(rates,maturities=eachindex(rates)) - # bump to a constant yield if only given one rate - length(rates) == 1 && return Constant(first(rates)) - return Yields.Forward(Bootstrap(),rates,maturities) -end - -""" - Zero(rates, maturities=eachindex(rates)) - Zero(p::YieldCurveFitParameters,rates, maturities=eachindex(rates)) - - -Construct a yield curve with given zero-coupon spot `rates` at the given `maturities`. The method of fitting the curve to the data is determined by the [`YieldCurveFitParameters`](@ref) object `p`, which is a `Boostrap(QuadraticSpline())` by default. - -If `rates` is a vector of floating point number instead of a vector `Rate`s, see the [`YieldCurveFitParameters`](@ref) for how the rate will be interpreted. - -# Examples - -```julia-repl -julia> Yields.Zero([0.01,0.02,0.04,0.05],[1,2,5,10]) - - ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀Yield Curve (Yields.BootstrapCurve)⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ - ┌────────────────────────────────────────────────────────────┐ - 0.05 │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠤⠒⠒⠒⠒⠒⠤⠤⠤⢄⣀⣀⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│ Zero rates - │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣀⠖⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠉⠉⠉⠒⠒⠒⠢⠤⠤⠤⣄⣀⣀⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│ - │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⠖⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠉⠉⠑⠒⠒⠒⠦⠤⠤⠤⣀⣀⣀⡀⠀│ - │⠀⠀⠀⠀⠀⠀⠀⠀⠀⡔⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠉│ - │⠀⠀⠀⠀⠀⠀⠀⢠⠎⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│ - │⠀⠀⠀⠀⠀⠀⢠⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│ - │⠀⠀⠀⠀⠀⢠⠃⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│ - Continuous │⠀⠀⠀⠀⢀⠇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│ - │⠀⠀⠀⠀⡜⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│ - │⠀⠀⠀⡸⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│ - │⠀⠀⢠⠇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│ - │⠀⠀⡜⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│ - │⠒⠒⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│ - │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│ - 0 │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│ - └────────────────────────────────────────────────────────────┘ - ⠀0⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀time⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀30⠀ - -``` -""" -function Yields.Zero(rates,maturities=eachindex(rates)) - # bump to a constant yield if only given one rate - length(rates) == 1 && return Constant(first(rates)) - return Yields.Zero(Bootstrap(),rates,maturities) -end - - -""" - Yields.CMT(rates, maturities; interpolation=QuadraticSpline()) - Yields.CMT(p::YieldCurveFitParameters,rates, maturities) - -Takes constant maturity (treasury) yields (bond equivalent), and assumes that instruments <= one year maturity pay no coupons and that the rest pay semi-annual. - -The method of fitting the curve to the data is determined by the [`YieldCurveFitParameters`](@ref) object `p`, which is a `Boostrap(QuadraticSpline())` by default. - -# Examples - -```julia-repl -# 2021-03-31 rates from Treasury.gov -rates =[0.01, 0.01, 0.03, 0.05, 0.07, 0.16, 0.35, 0.92, 1.40, 1.74, 2.31, 2.41] ./ 100 -mats = [1/12, 2/12, 3/12, 6/12, 1, 2, 3, 5, 7, 10, 20, 30] - -Yields.CMT(rates,mats) -``` -""" -function Yields.CMT(rates,maturities=eachindex(rates)) - # bump to a constant yield if only given one rate - length(rates) == 1 && return Constant(first(rates)) - return Yields.CMT(Bootstrap(),rates,maturities) -end - - -""" - OIS(rates, maturities) - OIS(p::YieldCurveFitParameters, rates, maturities) - -Takes Overnight Index Swap rates, and assumes that instruments <= one year maturity are settled once and other agreements are settled quarterly with a corresponding CompoundingFrequency. - -The method of fitting the curve to the data is determined by the [`YieldCurveFitParameters`](@ref) object `p`, which is a `Boostrap(QuadraticSpline())` by default. - -# Examples -```julia-repl -julia> ois = [1.8, 2.0, 2.2, 2.5, 3.0, 4.0] ./ 100; -julia> mats = [1 / 12, 1 / 4, 1 / 2, 1, 2, 5]; -julia> curve = Yields.OIS(ois, mats) - - ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀Yield Curve (Yields.BootstrapCurve)⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ - ┌────────────────────────────────────────────────────────────┐ - 0.1 │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│ Zero rates - │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⡤│ - │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⡤⠔⠒⠋⠁⠀⠀│ - │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⡤⠔⠒⠋⠁⠀⠀⠀⠀⠀⠀⠀⠀│ - │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⡤⠔⠒⠋⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│ - │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⡤⠔⠒⠋⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│ - │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⡤⠔⠒⠋⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│ - Continuous │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⡤⠔⠒⠋⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│ - │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⡤⠔⠒⠋⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│ - │⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⡤⠔⠒⠋⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│ - │⠀⠀⠀⠀⠀⠀⡠⠔⠊⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│ - │⠀⠀⠀⣀⠔⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│ - │⠀⡠⠎⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│ - │⠜⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│ - 0.01 │⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀│ - └────────────────────────────────────────────────────────────┘ - ⠀0⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀time⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀30⠀ -``` -""" -function Yields.OIS(rates,maturities=eachindex(rates)) - # bump to a constant yield if only given one rate - length(rates) == 1 && return Constant(first(rates)) - return Yields.OIS(Bootstrap(),rates,maturities) -end diff --git a/src/model/Equity.jl b/src/model/Equity.jl new file mode 100644 index 00000000..9b92c821 --- /dev/null +++ b/src/model/Equity.jl @@ -0,0 +1,21 @@ + +module Equity +import ..AbstractModel + +abstract type AbstractEquityModel <: AbstractModel end + +struct BlackScholesMerton{T,U,V} <: AbstractEquityModel + r::T # risk free rate + q::U # dividend yield + σ::V # roughly equivalent to the volatility in the usual lognormal model multiplied by F^{1-β}_{0} +end +end + +function volatility(vol::Volatility.Constant, strike_ratio, time_to_maturity) + return vol.σ +end + +function FinanceCore.present_value(model::M, c::Option.EuroCall{CommonEquity,K,T}) where {M<:Equity.BlackScholesMerton,K,T} + eurocall(; S=1.0, K=c.strike, τ=c.maturity, r=model.r, q=model.q, σ=model.σ) + +end \ No newline at end of file diff --git a/src/model/Model.jl b/src/model/Model.jl new file mode 100644 index 00000000..6e8825d8 --- /dev/null +++ b/src/model/Model.jl @@ -0,0 +1,27 @@ +abstract type AbstractModel end +Base.Broadcast.broadcastable(x::T) where {T<:AbstractModel} = Ref(x) + +# a model for when you don't really need a model +# (e.g. determining nominal cashflows for fixed income contract) +struct NullModel <: AbstractModel end + +struct DateModel{M,T} <: AbstractModel + model::M + date::T +end + +# useful for round-tripping or iterating on quotes? +function Quote(m::M, c::C) where {M<:AbstractModel,C<:FinanceCore.AbstractContract} + return Quote(pv(m, c), c) +end + +include("Spline.jl") +include("Yield.jl") +include("Volatility.jl") +include("Equity.jl") + +function FinanceCore.present_value(model, c::FinanceCore.AbstractContract, cur_time=0.0) + p = Projection(c, model, CashflowProjection()) + xf = p |> Filter(cf -> cf.time >= cur_time) |> Map(cf -> FinanceCore.discount(model, cur_time, cf.time) * cf.amount) + foldxl(+, xf) +end \ No newline at end of file diff --git a/src/model/Spline.jl b/src/model/Spline.jl new file mode 100644 index 00000000..202b7114 --- /dev/null +++ b/src/model/Spline.jl @@ -0,0 +1,18 @@ +module Spline +import ..FinanceCore +import ..BSplineKit +import ..AbstractModel + + +struct BSpline + order::Int +end + +Linear() = BSpline(2) +Quadratic() = BSpline(3) +Cubic() = BSpline(4) + + +# used as the object which gets optmized before finally returning a completed spline + +end \ No newline at end of file diff --git a/src/model/Volatility.jl b/src/model/Volatility.jl new file mode 100644 index 00000000..784e4472 --- /dev/null +++ b/src/model/Volatility.jl @@ -0,0 +1,11 @@ + +module Volatility +import ..AbstractModel + +abstract type AbstractVolatilityModel <: AbstractModel end + +struct Constant{T} <: AbstractVolatilityModel + σ::T +end +Constant() = Constant(0.0) +end \ No newline at end of file diff --git a/src/model/Yield.jl b/src/model/Yield.jl new file mode 100644 index 00000000..b35505bf --- /dev/null +++ b/src/model/Yield.jl @@ -0,0 +1,354 @@ + +module Yield +import ..AbstractModel +import ..FinanceCore +import ..Spline as Sp +import ..BSplineKit +import UnicodePlots +import ..Bond: coupon_times + +using FinanceCore: Continuous, Periodic, discount, accumulation, AbstractContract + +export discount, zero, forward, par, pv + +abstract type AbstractYieldModel <: AbstractModel end + + +struct Constant{R} <: AbstractYieldModel + rate::R +end + +function Constant(rate::R) where {R<:Real} + Constant(FinanceCore.Rate(rate)) +end + +Constant() = Constant(0.0) + +FinanceCore.discount(c::Constant, t) = FinanceCore.discount(c.rate, t) + +# used as the object which gets optmized before finally returning a completed spline +struct IntermediateYieldCurve{U,V} <: AbstractYieldModel + b::Sp.BSpline + xs::Vector{U} + ys::Vector{V} # here, ys are the discount factors +end + +function FinanceCore.discount(ic::IntermediateYieldCurve, time) + zs = zero_vec = -log.(clamp.(ic.ys, 0.00001, 1)) ./ ic.xs + c = Yield.Spline(ic.b, ic.xs, zs) + return exp(-c.fn(time) * time) +end + +struct Spline{U} <: AbstractYieldModel + fn::U # here, fn is a map from time to instantaneous zero rate +end + +function (c::Spline)(time) + c.fn(time) + return exp(-c.fn(time) * time) +end + +function FinanceCore.discount(c::Spline, time) + z = c.fn(time) + return exp(-z * time) +end + +function Spline(b::Sp.BSpline, xs, ys) + order = min(length(xs), b.order) # in case the length of xs is less than the spline order + int = BSplineKit.interpolate(xs, ys, BSplineKit.BSplineOrder(order)) + return Spline(BSplineKit.extrapolate(int, BSplineKit.Smooth())) +end + + +include("Yield/SmithWilson.jl") +include("Yield/NelsonSiegelSvensson.jl") + +## Generic and Fallbacks +""" + discount(yc, to) + discount(yc, from,to) + +The discount factor for the yield curve `yc` for times `from` through `to`. +""" +FinanceCore.discount(yc::T, from, to) where {T<:AbstractYieldModel} = discount(yc, to) / discount(yc, from) + +""" + forward(yc, from, to)˚ + +The forward `Rate` implied by the yield curve `yc` between times `from` and `to`. +""" +function FinanceCore.forward(yc::T, from, to=from + 1) where {T<:AbstractYieldModel} + Continuous(log(discount(yc, from) / discount(yc, to)) / (to - from)) +end + +""" + par(curve,time;frequency=2) + +Calculate the par yield for maturity `time` for the given `curve` and `frequency`. Returns a `Rate` object with periodicity corresponding to the `frequency`. The exception to this is if `time` is less than what the payments allowed by frequency (e.g. a time `0.5` but with frequency `1`) will effectively assume frequency equal to 1 over `time`. + +# Examples + +```julia-repl +julia> c = Yields.Constant(0.04); + +julia> Yields.par(c,4) +Yields.Rate{Float64, Yields.Periodic}(0.03960780543711406, Yields.Periodic(2)) + +julia> Yields.par(c,4;frequency=1) +Yields.Rate{Float64, Yields.Periodic}(0.040000000000000036, Yields.Periodic(1)) + +julia> Yields.par(c,0.6;frequency=4) +Yields.Rate{Float64, Yields.Periodic}(0.039413626195875295, Yields.Periodic(4)) + +julia> Yields.par(c,0.2;frequency=4) +Yields.Rate{Float64, Yields.Periodic}(0.039374942589460726, Yields.Periodic(5)) + +julia> Yields.par(c,2.5) +Yields.Rate{Float64, Yields.Periodic}(0.03960780543711406, Yields.Periodic(2)) +``` +""" +function par(curve, time; frequency=2) + mat_disc = discount(curve, time) + coup_times = coupon_times(time, frequency) + coupon_pv = sum(discount(curve, t) for t in coup_times) + Δt = step(coup_times) + r = (1 - mat_disc) / coupon_pv + cfs = [t == last(coup_times) ? 1 + r : r for t in coup_times] + # `sign(r)`` is used instead of `1` because there are times when the coupons are negative so we want to flip the sign + cfs = [-1; cfs] + r = FinanceCore.internal_rate_of_return(cfs, [0; coup_times]) + frequency_inner = min(1 / Δt, max(1 / Δt, frequency)) + r = convert(Periodic(frequency_inner), r) + return r +end + +""" + zero(curve,time) + +Return the zero rate for the curve at the given time. +""" +function Base.zero(c::YC, time) where {YC<:AbstractYieldModel} + df = discount(c, time) + r = -log(df) / time + return Continuous(r) +end + +""" + accumulation(yc, from, to) + +The accumulation factor for the yield curve `yc` for times `from` through `to`. +""" +function FinanceCore.accumulation(yc::AbstractYieldModel, time) + return 1 ./ discount(yc, time) +end + +function FinanceCore.accumulation(yc::AbstractYieldModel, from, to) + return 1 ./ discount(yc, from, to) +end + +## Curve Manipulations +""" + CompositeYield(curve1,curve2,operation) + +Creates a datastructure that will perform the given `operation` after independently calculating the effects of the two curves. +Can only be created via the public API by using the `+`, `-`, `*`, and `/` operatations on `AbstractYield` objects. + +As this is double the normal operations when performing calculations, if you are using the curve in performance critical locations, you should consider transforming the inputs and +constructing a single curve object ahead of time. +""" +struct CompositeYield{T,U,V} <: AbstractYieldModel + r1::T + r2::U + op::V +end + + +function FinanceCore.discount(rc::CompositeYield, time) + a1 = discount(rc.r1, time)^(-1 / time) - 1 + a2 = discount(rc.r2, time)^(-1 / time) - 1 + return 1 / (1 + rc.op(a1, a2))^time +end + + +""" + ForwardStarting(curve,forwardstart) + +Rebase a `curve` so that `discount`/`accumulation`/etc. are re-based so that time zero from the new curves perspective is the given `forwardstart` time. + +# Examples + +```julia-repl +julia> zero = [5.0, 5.8, 6.4, 6.8] ./ 100 +julia> maturity = [0.5, 1.0, 1.5, 2.0] +julia> curve = Yields.Zero(zero, maturity) +julia> fwd = Yields.ForwardStarting(curve, 1.0) + +julia> FinanceCore.discount(curve,1,2) +0.9275624570410582 + +julia> FinanceCore.discount(fwd,1) # `curve` has effectively been reindexed to `1.0` +0.9275624570410582 +``` + +# Extended Help + +While `ForwardStarting` could be nested so that, e.g. the third period's curve is the one-period forward of the second period's curve, it will be more efficient to reuse the initial curve from a runtime and compiler perspective. + +`ForwardStarting` is not used to construct a curve based on forward rates. See [`Forward`](@ref) instead. +""" +struct ForwardStarting{T,U} <: AbstractYieldModel + curve::U + forwardstart::T +end + +function FinanceCore.discount(c::ForwardStarting, to) + FinanceCore.discount(c.curve, c.forwardstart, to + c.forwardstart) +end + +""" + Yields.AbstractYieldModel + Yields.AbstractYieldModel + +The addition of two yields will create a `CompositeYield`. For `rate`, `discount`, and `accumulation` purposes the spot rates of the two curves will be added together. +""" +function Base.:+(a::AbstractYieldModel, b::AbstractYieldModel) + return CompositeYield(a, b, +) +end + +function Base.:+(a::Constant, b::Constant) + return Constant(a.rate + b.rate) +end + +function Base.:+(a::T, b) where {T<:AbstractYieldModel} + return a + Constant(b) +end + +function Base.:+(a, b::T) where {T<:AbstractYieldModel} + return Constant(a) + b +end + +""" + Yields.AbstractYieldModel * Yields.AbstractYieldModel + +The multiplication of two yields will create a `CompositeYield`. For `rate`, `discount`, and `accumulation` purposes the spot rates of the two curves will be added together. This can be useful, for example, if you wanted to after-tax a yield. + +# Examples + +```julia-repl +julia> m = Yields.Constant(0.01) * 0.79; + +julia> accumulation(m,1) +1.0079 + +julia> accumulation(.01*.79,1) +1.0079 +``` +""" +function Base.:*(a::AbstractYieldModel, b::AbstractYieldModel) + return CompositeYield(a, b, *) +end + +function Base.:*(a::Constant, b::Constant) + a_kind = a.rate.compounding + rate_new_basis = FinanceCore.rate(convert(a_kind, b.rate)) + return Constant( + FinanceCore.Rate( + FinanceCore.rate(a.rate) * rate_new_basis, + a_kind + ) + ) +end + +function Base.:*(a::T, b) where {T<:AbstractYieldModel} + return a * Constant(b) +end + +function Base.:*(a, b::T) where {T<:AbstractYieldModel} + return Constant(a) * b +end + +""" + Yields.AbstractYieldModel - Yields.AbstractYieldModel + +The subtraction of two yields will create a `CompositeYield`. For `rate`, `discount`, and `accumulation` purposes the spot rates of the second curves will be subtracted from the first. +""" +function Base.:-(a::AbstractYieldModel, b::AbstractYieldModel) + return CompositeYield(a, b, -) +end + +function Base.:-(a::Constant, b::Constant) + Constant(a.rate - b.rate) +end + +function Base.:-(a::T, b) where {T<:AbstractYieldModel} + return a - Constant(b) +end + +function Base.:-(a, b::T) where {T<:AbstractYieldModel} + return Constant(a) - b +end + +""" + Yields.AbstractYieldModel / Yields.AbstractYieldModel + +The division of two yields will create a `CompositeYield`. For `rate`, `discount`, and `accumulation` purposes the spot rates of the two curves will have the first divided by the second. This can be useful, for example, if you wanted to gross-up a yield to be pre-tax. + +# Examples + +```julia-repl +julia> m = Yields.Constant(0.01) / 0.79; + +julia> accumulation(d,1) +1.0126582278481013 + +julia> accumulation(.01/.79,1) +1.0126582278481013 +``` +""" +function Base.:/(a::AbstractYieldModel, b::AbstractYieldModel) + return CompositeYield(a, b, /) +end + +function Base.:/(a::Constant, b::Constant) + a_kind = a.rate.compounding + rate_new_basis = FinanceCore.rate(convert(a_kind, b.rate)) + return Constant( + FinanceCore.Rate( + FinanceCore.rate(a.rate) / rate_new_basis, + a_kind + ) + ) +end + +function Base.:/(a::T, b) where {T<:AbstractYieldModel} + return a / Constant(b) +end + +function Base.:/(a, b::T) where {T<:AbstractYieldModel} + return Constant(a) / b +end + + +# used to display simple type name in show method +# https://stackoverflow.com/questions/70043313/get-simple-name-of-type-in-julia?noredirect=1#comment123823820_70043313 +name(::Type{T}) where {T} = (isempty(T.parameters) ? T : T.name.wrapper) + +function Base.show(io::IO, curve::T) where {T<:AbstractYieldModel} + println() # blank line for padding + r = zero(curve, 1) + ylabel = isa(r.compounding, Continuous) ? "Continuous" : "Periodic($(r.compounding.frequency))" + kind = name(typeof(curve)) + l = UnicodePlots.lineplot( + 0.0, #from + 30.0, # to + t -> FinanceCore.rate(zero(curve, t)), + xlabel="time", + ylabel=ylabel, + compact=true, + name="Zero rates", + width=60, + title="Yield Curve ($kind)" + ) + show(io, l) +end + +end diff --git a/src/model/Yield/NelsonSiegelSvensson.jl b/src/model/Yield/NelsonSiegelSvensson.jl new file mode 100644 index 00000000..0ba40f74 --- /dev/null +++ b/src/model/Yield/NelsonSiegelSvensson.jl @@ -0,0 +1,121 @@ +# NS and NSS +## Originally developed by leeyuntien + +""" + NelsonSiegel(β₀, β₁, β₂, τ₁) + NelsonSiegel(τ₁=1.0) # used in fitting + + +A Nelson-Siegel yield curve model +Parameters of Nelson and Siegel (1987) parametric model: + +- β₀ represents a long-term interest rate +- β₁ represents a time-decay component +- β₂ represents a hump +- τ₁ controls the location of the hump + +# Examples + +```julia-repl +julia> β₀, β₁, β₂, τ₁ = 0.6, -1.2, -1.9, 3.0 +julia> nsm = Yields.NelsonSiegel.(β₀, β₁, β₂, τ₁) + +# Extended Help + +NelsonSiegel has generally been replaced by NelsonSiegelSvensson, which is a more flexible model. + +## References +- https://onriskandreturn.com/2019/12/01/nelson-siegel-yield-curve-model/ +- https://www.bis.org/publ/bppdf/bispap25.pdf + +``` +""" +struct NelsonSiegel{T} <: AbstractYieldModel + τ₁::T + β₀::T + β₁::T + β₂::T + + function NelsonSiegel(τ₁::T, β₀::T, β₁::T, β₂::T) where {T<:Real} + (τ₁ <= 0) && throw(DomainError("Wrong tau parameter ranges (must be positive)")) + return new{T}(τ₁, β₀, β₁, β₂) + end +end + +function NelsonSiegel(τ₁=1.0) + return NelsonSiegel(τ₁, 1.0, 0.0, 0.0) +end + +function Base.zero(ns::NelsonSiegel, t) + if iszero(t) + # zero rate is undefined for t = 0 + t += eps() + end + Continuous.(ns.β₀ .+ ns.β₁ .* (1.0 .- exp.(-t ./ ns.τ₁)) ./ (t ./ ns.τ₁) .+ ns.β₂ .* ((1.0 .- exp.(-t ./ ns.τ₁)) ./ (t ./ ns.τ₁) .- exp.(-t ./ ns.τ₁))) +end +FinanceCore.discount(ns::NelsonSiegel, t) = discount.(zero.(ns, t), t) + +""" + NelsonSiegelSvensson(τ₁, τ₂, β₀, β₁, β₂, β₃) + NelsonSiegelSvensson(τ₁=1.0, τ₂=1.0) + +Return the NelsonSiegelSvensson yield curve. The rates should be continuous zero spot rates. If `rates` are not `Rate`s, then they will be interpreted as `Continuous` `Rate`s. + +Parameters of Svensson (1994) parametric model: + +- τ₁ controls the location of the hump +- τ₁ controls the location of the second hump +- β₀ represents a long-term interest rate +- β₁ represents a time-decay component +- β₂ represents a hump +- β₃ represents a second hum + +# Examples + +```julia-repl +julia> β₀, β₁, β₂, β₃, τ₁, τ₂ = 0.6, -1.2, -2.1, 3.0, 1.5 +julia> nssm = NelsonSiegelSvensson.NelsonSiegelSvensson.(β₀, β₁, β₂, β₃, τ₁, τ₂) + +# Extended Help + +Nelson-Siegel-Svensson Pros: + +- Simplicity: With only six parameters, the model is quite parsimonious and easy to estimate. It's also easier to interpret and communicate than more complex models. +- Economic Interpretability: Each of the model's components can be given an economic interpretation, with parameters representing long term rate, short term rate, the rates of decay towards the long term rate, and humps in the yield curve. + +Nelson-Siegel-Svensson Cons: + +- Unusual Curves: NSS makes some assumptions about the shape of the yield curve (e.g. generally has a hump in short to medium term maturities). It might not be the best choice for fitting unusual curves. +- Arbitrage Opportunities: The NSS model does not guarantee absence of arbitrage opportunities. More sophisticated models, like the ones based on no-arbitrage conditions, might provide better pricing accuracy in some contexts. +- Sensitivity: Similar inputs may produce different parameters due to the highly convex, non-linear region to solve for the parameters. Entities like the ECB will partially mitigate this by using the prior business day's parameters as the starting point for the current day's yield curve. + +## References +- https://onriskandreturn.com/2019/12/01/nelson-siegel-yield-curve-model/ +- https://www.bis.org/publ/bppdf/bispap25.pdf + +``` +""" +struct NelsonSiegelSvensson{T} <: AbstractYieldModel + τ₁::T + τ₂::T + β₀::T + β₁::T + β₂::T + β₃::T + + function NelsonSiegelSvensson(τ₁::T, τ₂::T, β₀::T, β₁::T, β₂::T, β₃::T) where {T} + (τ₁ <= 0 || τ₂ <= 0) && throw(DomainError("Wrong tau parameter ranges (must be positive)")) + return new{T}(τ₁, τ₂, β₀, β₁, β₂, β₃) + end +end + +NelsonSiegelSvensson(τ₁=1.0, τ₂=1.0) = NelsonSiegelSvensson(τ₁, τ₂, 0.0, 0.0, 0.0, 0.0) + +function Base.zero(nss::NelsonSiegelSvensson, t) + if iszero(t) + # zero rate is undefined for t = 0 + t += eps() + end + Continuous.(nss.β₀ .+ nss.β₁ .* (1.0 .- exp.(-t ./ nss.τ₁)) ./ (t ./ nss.τ₁) .+ nss.β₂ .* ((1.0 .- exp.(-t ./ nss.τ₁)) ./ (t ./ nss.τ₁) .- exp.(-t ./ nss.τ₁)) .+ nss.β₃ .* ((1.0 .- exp.(-t ./ nss.τ₂)) ./ (t ./ nss.τ₂) .- exp.(-t ./ nss.τ₂))) +end +FinanceCore.discount(nss::NelsonSiegelSvensson, t) = discount.(zero.(nss, t), t) \ No newline at end of file diff --git a/src/model/Yield/SmithWilson.jl b/src/model/Yield/SmithWilson.jl new file mode 100644 index 00000000..f079cc46 --- /dev/null +++ b/src/model/Yield/SmithWilson.jl @@ -0,0 +1,97 @@ +# SmithWilson +## Originally developed by kasperrisager + + +using ..LinearAlgebra +using ..FinanceCore + +""" + SmithWilson(zcq::Vector{ZeroCouponQuote}; ufr, α) + SmithWilson(swq::Vector{SwapQuote}; ufr, α) + SmithWilson(bbq::Vector{BulletBondQuote}; ufr, α) + SmithWilson(times<:AbstractVector, cashflows<:AbstractMatrix, prices<:AbstractVector; ufr, α) + SmithWilson(u, qb; ufr, α) + +Create a yield curve object that implements the Smith-Wilson interpolation/extrapolation scheme. + +Positional arguments to construct a curve: + +- Quoted instrument as the first argument: either a `Vector` of `ZeroCouponQuote`s, `SwapQuote`s, or `BulletBondQuote`s, or +- A set of `times`, `cashflows`, and `prices`, or +- A curve can be with `u` is the timepoints coming from the calibration, and `qb` is the internal parameterization of the curve that ensures that the calibration is correct. Users may prefer the other constructors but this mathematical constructor is also available. + +Required keyword arguments: + +- `ufr` is the Ultimate Forward Rate, the forward interest rate to which the yield curve tends, in continuous compounding convention. +- `α` is the parameter that governs the speed of convergence towards the Ultimate Forward Rate. It can be typed with `\\alpha[TAB]` + +# Extended Help + +## References + +- [Smith-Wilson Yields Curves](http://gli.lu/2017/12/smith-wilson-yield-curves/) +- [A Technical Note on the Smith-Wilson Method](http://www.ressources-actuarielles.net/EXT/ISFA/fp-isfa.nsf/2b0481298458b3d1c1256f8a0024c478/bd689cce9bb2aeb5c1257998001ede2b/\$FILE/A_Technical_Note_on_the_Smith-Wilson_Method_100701.pdf) + +""" +struct SmithWilson{TU<:AbstractVector,TQb<:AbstractVector,U,A} <: AbstractYieldModel + u::TU + qb::TQb + ufr::U + α::A + + # Inner constructor ensures that vector lengths match + function SmithWilson(u::TU, qb::TQb, ufr::U, α::A) where {TU<:AbstractVector,TQb<:AbstractVector,U,A} + if length(u) != length(qb) + throw(DomainError("Vectors u and qb in SmithWilson must have equal length")) + end + return new{TU,TQb,U,A}(u, qb, ufr, α) + end +end + + +function SmithWilson(u, qb; ufr, α) + return SmithWilson(u, qb, ufr, α) +end + +# uninitialized rates used for `fit` +function SmithWilson(; ufr, α) + return SmithWilson(Float64[], Float64[]; ufr, α) +end + + +function SmithWilson(times::AbstractVector, cashflows::AbstractMatrix, prices::AbstractVector; ufr, α) + Q = Diagonal(exp.(-ufr * times)) * cashflows + q = vec(sum(Q, dims=1)) # We want q to be a column vector + QHQ = Q' * H(α, times) * Q + b = QHQ \ (prices - q) + Qb = Q * b + return SmithWilson(times, Qb; ufr=ufr, α=α) +end + +FinanceCore.discount(sw::SmithWilson, t) = exp(-sw.ufr * t) * (1.0 + H(sw.α, sw.u, t) ⋅ sw.qb) + + +""" + H_ordered(α, t_min, t_max) + +The Smith-Wilson H function with ordered arguments (for better performance than using min and max). +""" +function H_ordered(α, t_min, t_max) + return α * t_min + exp(-α * t_max) * sinh(-α * t_min) +end + +""" + H(α, t1, t2) + +The Smith-Wilson H function implemented in a faster way. +""" +function H(α, t1::T, t2::T) where {T} + return t1 < t2 ? H_ordered(α, t1, t2) : H_ordered(α, t2, t1) +end + +H(α, t1, t2) = H(α, promote(t1, t2)...) + +H(α, t1vec::AbstractVector, t2) = [H(α, t1, t2) for t1 in t1vec] +H(α, t1vec::AbstractVector, t2vec::AbstractVector) = [H(α, t1, t2) for t1 in t1vec, t2 in t2vec] +# This can be optimized by going to H_ordered directly, but it might be a bit cumbersome +H(α, tvec::AbstractVector) = H(α, tvec, tvec) \ No newline at end of file diff --git a/src/precompiles.jl b/src/precompiles.jl deleted file mode 100644 index d3bc1ea4..00000000 --- a/src/precompiles.jl +++ /dev/null @@ -1,26 +0,0 @@ - -# created with the help of SnoopCompile.jl -@setup_workload begin - - - # 2021-03-31 rates from Treasury.gov - rates =[0.01, 0.01, 0.03, 0.05, 0.07, 0.16, 0.35, 0.92, 1.40, 1.74, 2.31, 2.41] ./ 100 - tenors = [1/12, 2/12, 3/12, 6/12, 1, 2, 3, 5, 7, 10, 20, 30] - - @compile_workload begin - # all calls in this block will be precompiled, regardless of whether - # they belong to your package or not (on Julia 1.8 and higher) - Yields.Par(rates,tenors) - Yields.CMT(rates,tenors) - Yields.Forward(rates,tenors) - Yields.OIS(rates,tenors) - Yields.Zero(NelsonSiegel(), rates,tenors) - Yields.Zero(NelsonSiegelSvensson(), rates,tenors) - Yields.Zero(rates,tenors) - c = Yields.Zero(rates,tenors) - Yields.zero(c,10) - Yields.par(c,10) - Yields.forward(c,5,6) - end - -end \ No newline at end of file diff --git a/src/utils.jl b/src/utils.jl index cd502339..bc7ef092 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -1,183 +1,78 @@ -# make interest curve broadcastable so that you can broadcast over multiple`time`s in `interest_rate` -Base.Broadcast.broadcastable(ic::T) where {T<:AbstractYieldCurve} = Ref(ic) - -function coupon_times(time,frequency) - Δt = min(1 / frequency,time) - times = time:-Δt:0 - f = last(times) - f += iszero(f) ? Δt : zero(f) - l = first(times) - return f:Δt:l -end - -# internal function (will be used in EconomicScenarioGenerators) -# defines the rate output given just the type of curve -__ratetype(::Type{Rate{T,U}}) where {T,U<:Periodic} = Yields.Rate{T, Periodic} -__ratetype(::Type{Rate{T,U}}) where {T,U<:Continuous} = Yields.Rate{T, Continuous} -__ratetype(curve::T) where {T<:AbstractYieldCurve} = __ratetype(typeof(curve)) -__ratetype(::Type{T}) where {T<:AbstractYieldCurve} = Yields.Rate{Float64, typeof(DEFAULT_COMPOUNDING)} - -# https://github.com/dpsanders/hands_on_julia/blob/master/during_sessions/Fractale%20de%20Newton.ipynb -newton(f, f′, x) = x - f(x) / f′(x) -function solve(g, g′, x0, max_iterations = 100) - x = x0 +N(x) = cdf(Normal(), x) - tolerance = 2 * eps(x0) - iteration = 0 - - while (abs(g(x) - 0) > tolerance && iteration < max_iterations) - x = newton(g, g′, x) - iteration += 1 - end +function d1(S, K, τ, r, σ, q) + return (log(S / K) + (r - q + σ^2 / 2) * τ) / (σ * √(τ)) +end - return x +function d2(S, K, τ, r, σ, q) + return d1(S, K, τ, r, σ, q) - σ * √(τ) end +""" + eurocall(;S=1.,K=1.,τ=1,r,σ,q=0.) -# convert to a given rate type if not already a rate -__coerce_rate(x::T,cf) where {T<:Rate} = return x -__coerce_rate(x,cf) = return Rate(x,cf) +Calculate the Black-Scholes implied option price for a european call, where: +- `S` is the current asset price +- `K` is the strike or exercise price +- `τ` is the time remaining to maturity (can be typed with \\tau[tab]) +- `r` is the continuously compounded risk free rate +- `σ` is the (implied) volatility (can be typed with \\sigma[tab]) +- `q` is the continuously paid dividend rate -abstract type InterpolationKind end +Rates should be input as rates (not percentages), e.g.: `0.05` instead of `5` for a rate of five percent. -struct QuadraticSpline <: InterpolationKind end -struct LinearSpline <: InterpolationKind end +!!! Experimental: this function is well-tested, but the derivatives functionality (API) may change in a future version of ActuaryUtilities. -""" - bootstrap(rates, maturities, settlement_frequency, interpolation::QuadraticSpline()) +# Extended Help -Bootstrap the rates with the given maturities, treating the rates according to the periodic frequencies in settlement_frequency. +This is the same as the formulation presented in the [dividend extension of the BS model in Wikipedia](https://en.wikipedia.org/wiki/Black%E2%80%93Scholes_model#Black%E2%80%93Scholes_equation). -`interpolator` is any function that will take two vectors of inputs and output points and return a function that will estimate an output given a scalar input. That is -`interpolator` should be: `interpolator(xs, ys) -> f(x)` where `f(x)` is the interpolated value of `y` at `x`. +## Other general comments: -Built in `interpolator`s in Yields are: -- `QuadraticSpline()`: Quadratic spline interpolation. -- `LinearSpline()`: Linear spline interpolation. +- Swap/OIS curves are generally better sources for `r` than government debt (e.g. US Treasury) due to the collateralized nature of swap instruments. +- (Implied) volatility is characterized by a curve that is a function of the strike price (among other things), so take care when using +- Yields.jl can assist with converting rates to continuously compounded if you need to perform conversions. -The default is `QuadraticSpline()`. """ -function bootstrap(rates, maturities, settlement_frequency, interpolation::InterpolationKind=QuadraticSpline()) - return _bootstrap_choose_interp(rates, maturities, settlement_frequency, interpolation) +function eurocall(; S=1.0, K=1.0, τ=1, r, σ, q=0.0) + iszero(τ) && return max(zero(S), S - K) + d₁ = d1(S, K, τ, r, σ, q) + d₂ = d2(S, K, τ, r, σ, q) + return (N(d₁) * S * exp(τ * (r - q)) - N(d₂) * K) * exp(-r * τ) end -# the fall-back if user provides own interpolation function -function bootstrap(rates, maturities, settlement_frequency, interpolation) - return _bootstrap_inner(rates, maturities, settlement_frequency, interpolation) -end - -# dispatch on the user-exposed InterpolationKind to the right -# internally named interpolation function -function _bootstrap_choose_interp(rates, maturities, settlement_frequency, i::QuadraticSpline) - return _bootstrap_inner(rates, maturities, settlement_frequency, cubic_interp) -end +""" + europut(;S=1.,K=1.,τ=1,r,σ,q=0.) -function _bootstrap_choose_interp(rates, maturities, settlement_frequency, i::LinearSpline) - return _bootstrap_inner(rates, maturities, settlement_frequency, linear_interp) -end +Calculate the Black-Scholes implied option price for a european call, where: +- `S` is the current asset price +- `K` is the strike or exercise price +- `τ` is the time remaining to maturity (can be typed with \\tau[tab]) +- `r` is the continuously compounded risk free rate +- `σ` is the (implied) volatility (can be typed with \\sigma[tab]) +- `q` is the continuously paid dividend rate +Rates should be input as rates (not percentages), e.g.: `0.05` instead of `5` for a rate of five percent. -function _bootstrap_inner(rates, maturities, settlement_frequency, interpolation_function) - discount_vec = zeros(length(rates)) # construct a placeholder discount vector matching maturities - # we have to take the first rate as the starting point - discount_vec[1] = discount(Constant(rates[1]), maturities[1]) - - for t = 2:length(maturities) - if isnothing(settlement_frequency[t]) - # no settlement before maturity - discount_vec[t] = discount(Constant(rates[t]), maturities[t]) - else - # need to account for the interim cashflows settled - times = settlement_frequency[t]:settlement_frequency[t]:maturities[t] - cfs = [rate(rates[t]) * settlement_frequency[t] for s in times] - cfs[end] += 1 - - function pv(v_guess) - v = interpolation_function([[0.0]; maturities[1:t]], vcat(1.0, discount_vec[1:t-1], v_guess...)) - return sum(v.(times) .* cfs) - end - target_pv = sum(map(t2 -> discount(Constant(rates[t]), t2), times) .* cfs) - root_func(v_guess) = pv(v_guess) - target_pv - root_func′(v_guess) = ForwardDiff.derivative(root_func, v_guess) - discount_vec[t] = solve(root_func, root_func′, rate(rates[t])) - end - - end - zero_vec = -log.(clamp.(discount_vec,0.00001,1)) ./ maturities - return interpolation_function([0.0; maturities], [first(zero_vec); zero_vec]) -end -# the ad-hoc approach to extrapoliatons is based on suggestion by author of -# BSplineKit at https://github.com/jipolanco/BSplineKit.jl/issues/19 -# this should not be exposed directly to user -struct _Extrap{I,L,R} - int::I # the BSplineKit interpolation - left::L # a tuple of (boundary, extrapolation function) - right::R # a tuple of (boundary, extrapolation function) -end +!!! Experimental: this function is well-tested, but the derivatives functionality (API) may change in a future version of ActuaryUtilities. -function _wrap_spline(itp) - - S = BSplineKit.spline(itp) # spline passing through data points - B = BSplineKit.basis(S) # B-spline basis - - a, b = BSplineKit.boundaries(B) # left and right boundaries - - # For now, we construct the full spline S′(x). - # There are faster ways of doing this that should be implemented... - S′ = diff(S, BSplineKit.Derivative(1)) - - return _Extrap(itp, - (boundary = a, func = x->S(a) + S′(a)*(x-a)), - (boundary = b, func = x->S(b) + S′(b)*(x-b)), - - ) -end +# Extended Help -function _interp(e::_Extrap,x) - if x <= e.left.boundary - return e.left.func(x) - elseif x >= e.right.boundary - return e.right.func(x) - else - return e.int(x) - end -end +This is the same as the formulation presented in the [dividend extension of the BS model in Wikipedia](https://en.wikipedia.org/wiki/Black%E2%80%93Scholes_model#Black%E2%80%93Scholes_equation). -function linear_interp(xs, ys) - int = BSplineKit.interpolate(xs, ys, BSplineKit.BSplineOrder(2)) - e = _wrap_spline(int) - return x -> _interp(e, x) -end +## Other general comments: -function cubic_interp(xs, ys) - order = min(length(xs),3) # in case the length of xs is less than the spline order - int = BSplineKit.interpolate(xs, ys, BSplineKit.BSplineOrder(order)) - e = _wrap_spline(int) - return x -> _interp(e, x) -end +- Swap/OIS curves are generally better sources for `r` than government debt (e.g. US Treasury) due to the collateralized nature of swap instruments. +- (Implied) volatility is characterized by a curve that is a function of the strike price (among other things), so take care when using +- Yields.jl can assist with converting rates to continuously compounded if you need to perform conversions. -# used to display simple type name in show method -# https://stackoverflow.com/questions/70043313/get-simple-name-of-type-in-julia?noredirect=1#comment123823820_70043313 -name(::Type{T}) where {T} = (isempty(T.parameters) ? T : T.name.wrapper) - -function Base.show(io::IO, curve::T) where {T<:AbstractYieldCurve} - println() # blank line for padding - r = zero(curve, 1) - ylabel = isa(r.compounding, Continuous) ? "Continuous" : "Periodic($(r.compounding.frequency))" - kind = name(typeof(curve)) - l = lineplot( - t -> rate(zero(curve, t)), - 0.0, #from - 30.0, # to - xlabel = "time", - ylabel = ylabel, - compact = true, - name = "Zero rates", - width = 60, - title = "Yield Curve ($kind)" - ) - show(io, l) +""" +function europut(; S=1.0, K=1.0, τ=1, r, σ, q=0.0) + iszero(τ) && return max(zero(S), K - S) + d₁ = d1(S, K, τ, r, σ, q) + d₂ = d2(S, K, τ, r, σ, q) + return (N(-d₂) * K - N(-d₁) * S * exp(τ * (r - q))) * exp(-r * τ) end \ No newline at end of file diff --git a/test/ActuaryUtilities.jl b/test/ActuaryUtilities.jl index 4db8d539..b5e3ca9f 100644 --- a/test/ActuaryUtilities.jl +++ b/test/ActuaryUtilities.jl @@ -2,15 +2,13 @@ using ActuaryUtilities @testset "ActuaryUtilities.jl integration tests" begin cfs = [5, 5, 105] - times = [1, 2, 3] - - discount_rates = [0.03,Yields.Periodic(0.03,1), Yields.Constant(0.03)] + times = [1, 2, 3] + discount_rates = [0.03, Periodic(0.03, 1), Yield.Constant(0.03)] for d in discount_rates - @test present_value(d, cfs, times) ≈ 105.65722270978935 - @test duration(Macaulay(), d, cfs, times) ≈ 2.86350467067113 - @test duration(d, cfs, times) ≈ 2.7801016220108057 - @test convexity(d, cfs, times) ≈ 10.625805482685939 + @test present_value(d, cfs, times) ≈ 105.65722270978935 + @test duration(Macaulay(), d, cfs, times) ≈ 2.86350467067113 + @test duration(d, cfs, times) ≈ 2.7801016220108057 + @test convexity(d, cfs, times) ≈ 10.625805482685939 end end - \ No newline at end of file diff --git a/test/CompositeYield.jl b/test/CompositeYield.jl new file mode 100644 index 00000000..98256cd0 --- /dev/null +++ b/test/CompositeYield.jl @@ -0,0 +1,54 @@ +@testset "Rate Combinations" begin + riskfree_maturities = [0.5, 1.0, 1.5, 2.0] + riskfree = [5.0, 5.8, 6.4, 6.8] ./ 100 # spot rates + rf_curve = fit(Spline.Linear(), ZCBYield.(riskfree, riskfree_maturities), Fit.Bootstrap()) + + @testset "base + spread" begin + + spread_maturities = [0.5, 1.0, 1.5, 3.0] # different maturities + spread = [1.0, 1.8, 1.4, 1.8] ./ 100 # spot spreads + + + spread_curve = fit(Spline.Linear(), ZCBYield.(spread, spread_maturities), Fit.Bootstrap()) + + yield = rf_curve + spread_curve + + @test zero(yield, 0.5) ≈ Periodic(first(riskfree) + first(spread), 1) + + @test discount(yield, 1.0) ≈ 1 / (1 + riskfree[2] + spread[2])^1 + @test discount(yield, 1.5) ≈ 1 / (1 + riskfree[3] + spread[3])^1.5 + end + + @testset "multiplicaiton and division" begin + @testset "multiplication" begin + factor = 0.79 + c = rf_curve * factor + target_curve = fit(Spline.Linear(), ZCBYield.(riskfree .* factor, riskfree_maturities), Fit.Bootstrap()) + @test discount(c, 2) ≈ discount(target_curve, 2) + @test accumulation(c, 2) ≈ accumulation(target_curve, 2) + @test forward(c, 1, 2) ≈ forward(target_curve, 1, 2) + @test par(c, 2) ≈ par(target_curve, 2) + + c = factor * rf_curve + @test discount(c, 2) ≈ discount(target_curve, 2) + @test accumulation(c, 2) ≈ accumulation(target_curve, 2) + @test forward(c, 1, 2) ≈ forward(target_curve, 1, 2) + @test par(c, 2) ≈ par(target_curve, 2) + + @test discount(Yield.Constant(0.1) * Yield.Constant(0.1), 10) ≈ discount(Yield.Constant(0.01), 10) + end + + @testset "division" begin + factor = 0.79 + c = rf_curve / factor + target_curve = fit(Spline.Linear(), ZCBYield.(riskfree ./ factor, riskfree_maturities), Fit.Bootstrap()) + @test discount(c, 2) ≈ discount(target_curve, 2) + @test accumulation(c, 2) ≈ accumulation(target_curve, 2) + @test forward(c, 1, 2) ≈ forward(target_curve, 1, 2) + @test par(c, 2) ≈ par(target_curve, 2) + + @test discount(Yield.Constant(0.1) / Yield.Constant(0.5), 10) ≈ discount(Yield.Constant(0.2), 10) + @test discount(0.1 / Yield.Constant(0.5), 10) ≈ discount(Yield.Constant(0.2), 10) + end + end +end \ No newline at end of file diff --git a/test/Equity.jl b/test/Equity.jl new file mode 100644 index 00000000..66fc1011 --- /dev/null +++ b/test/Equity.jl @@ -0,0 +1,50 @@ +@testset "Derivatives" begin + + @testset "Euro Options" begin + # tested against https://option-price.com/index.php + params = (S=1.0, K=1.0, τ=1, r=0.05, σ=0.25, q=0.0) + + @test eurocall(; params...) ≈ 0.12336 atol = 1e-5 + @test europut(; params...) ≈ 0.07459 atol = 1e-5 + + params = (S=1.0, K=1.0, τ=1, r=0.05, σ=0.25, q=0.03) + @test eurocall(; params...) ≈ 0.105493 atol = 1e-5 + @test europut(; params...) ≈ 0.086277 atol = 1e-5 + + params = (S=1.0, K=0.5, τ=1, r=0.05, σ=0.25, q=0.03) + @test eurocall(; params...) ≈ 0.49494 atol = 1e-5 + @test europut(; params...) ≈ 0.00011 atol = 1e-5 + + params = (S=1.0, K=0.5, τ=1, r=0.05, σ=0.25, q=0.03) + @test eurocall(; params...) ≈ 0.49494 atol = 1e-5 + @test europut(; params...) ≈ 0.00011 atol = 1e-5 + + params = (S=1.0, K=0.5, τ=0, r=0.05, σ=0.25, q=0.03) + @test eurocall(; params...) ≈ 0.5 atol = 1e-5 + @test europut(; params...) ≈ 0.0 atol = 1e-5 + + params = (S=1.0, K=1.5, τ=0, r=0.05, σ=0.25, q=0.03) + @test eurocall(; params...) ≈ 0.0 atol = 1e-5 + @test europut(; params...) ≈ 0.5 atol = 1e-5 + + end +end + + + +@testset "Equity Models" begin + m = Equity.BlackScholesMerton(0.01, 0.02, 0.15) + + a = Option.EuroCall(CommonEquity(), 1.0, 1.0) + b = Option.EuroCall(CommonEquity(), 1.0, 2.0) + + @test pv(m, a) ≈ 0.05410094201902403 + + qs = [ + Quote(0.0541, a), + Quote(0.072636, b), + ] + m = Equity.BlackScholesMerton(0.01, 0.02, Volatility.Constant()) + fit(m, qs) + @test fit(m, qs).σ ≈ 0.15 atol = 1e-4 +end \ No newline at end of file diff --git a/test/NelsonSiegelSvensson.jl b/test/NelsonSiegelSvensson.jl index cc76247e..a704b88b 100644 --- a/test/NelsonSiegelSvensson.jl +++ b/test/NelsonSiegelSvensson.jl @@ -2,52 +2,39 @@ # Per this technical note, the target/source rates should be interpreted as continuous rates: # https://www.ecb.europa.eu/stats/financial_markets_and_interest_rates/euro_area_yield_curves/html/technical_notes.pdf - + # EURAAA_20191111 at https://www.ecb.europa.eu/stats/financial_markets_and_interest_rates/euro_area_yield_curves/html/index.en.html - euraaa_zeros = Continuous.([-0.602009,-0.612954,-0.621543,-0.627864,-0.632655,-0.610565,-0.569424,-0.516078,-0.455969,-0.39315,-0.33047,-0.269814,-0.21234,-0.158674,-0.109075,-0.063552,-0.021963,0.015929,0.050407,0.081771,0.110319,0.136335,0.160083,0.181804,0.201715,0.220009,0.23686,0.252419,0.26682,0.280182,0.292608,0.304191,0.31501] ./ 100) - euraaa_pars = Continuous.([-0.601861,-0.612808,-0.621403,-0.627731,-0.63251,-0.610271,-0.568825,-0.515067,-0.454526,-0.391338,-0.328412,-0.26767,-0.210277,-0.156851,-0.107632,-0.062605,-0.021601,0.015642,0.049427,0.080073,0.107892,0.133178,0.156206,0.17722,0.196444,0.214073,0.230281,0.245221,0.259027,0.271818,0.283696,0.294753,0.305069] ./ 100) + euraaa_zeros = Continuous.([-0.602009, -0.612954, -0.621543, -0.627864, -0.632655, -0.610565, -0.569424, -0.516078, -0.455969, -0.39315, -0.33047, -0.269814, -0.21234, -0.158674, -0.109075, -0.063552, -0.021963, 0.015929, 0.050407, 0.081771, 0.110319, 0.136335, 0.160083, 0.181804, 0.201715, 0.220009, 0.23686, 0.252419, 0.26682, 0.280182, 0.292608, 0.304191, 0.31501] ./ 100) + euraaa_pars = Continuous.([-0.601861, -0.612808, -0.621403, -0.627731, -0.63251, -0.610271, -0.568825, -0.515067, -0.454526, -0.391338, -0.328412, -0.26767, -0.210277, -0.156851, -0.107632, -0.062605, -0.021601, 0.015642, 0.049427, 0.080073, 0.107892, 0.133178, 0.156206, 0.17722, 0.196444, 0.214073, 0.230281, 0.245221, 0.259027, 0.271818, 0.283696, 0.294753, 0.305069] ./ 100) euraaa_maturities = vcat([0.25, 0.5, 0.75], 1:30) - + zqs = ZCBYield.(euraaa_zeros, euraaa_maturities) @testset "NelsonSiegel" begin @testset "EURAAA_20191111" begin - c_param = Yields.NelsonSiegelCurve(0.6202603126029489 /100, -1.1621281759833935 /100, -1.930016080035979 /100, 3.0) - c = Yields.Zero(NelsonSiegel(),euraaa_zeros, euraaa_maturities) - # @testset "parameter: $param" for param in [:β₀, :β₁, :β₂, :τ₁] - # @test getfield(c, param) ≈ getfield(c_param, param) - # end + c_param = Yield.NelsonSiegel(3.0, 0.6202603126029489 / 100, -1.1621281759833935 / 100, -1.930016080035979 / 100) + c = fit(Yield.NelsonSiegel(), zqs) @testset "zero rates: $t" for (t, r) in zip(euraaa_maturities, euraaa_zeros) - @test Yields.zero(c, t) ≈ r atol = 0.001 + @test FinanceModels.zero(c, t) ≈ r atol = 0.005 + @test discount(c, t) ≈ discount(r, t) rtol = 0.0025 end @testset "par rates: $t" for (t, r) in zip(euraaa_maturities, euraaa_pars) - @test Yields.zero(c, t) ≈ r atol = 0.001 + @test FinanceModels.par(c, t) ≈ r atol = 0.005 end - @test discount(c,0) == 1.0 - @test discount(c,10) ≈ 1 / accumulation(c,10) - - @testset "misc constructors" begin - for u in [Yields.Forward, Yields.Zero, Yields.Par] - @test u(NelsonSiegel(), euraaa_zeros) isa Yields.AbstractYieldCurve - end - end + @test discount(c, 0) == 1.0 + @test discount(c, 10) ≈ 1 / accumulation(c, 10) - # test that rates as floats work - fzeros = Yields.rate.(euraaa_zeros) - c_fzero = Yields.Zero(NelsonSiegel(),fzeros, euraaa_maturities) - @test c_fzero == c end # Nelson-Siegel-Svensson package example at https://nelson-siegel-svensson.readthedocs.io/en/latest/usage.html @testset "pack" begin - pack_yields = Continuous.([0.01, 0.011, 0.013, 0.016, 0.019, 0.021, 0.026, 0.03, 0.035, 0.037, 0.038, 0.04]) + pack_FinanceModels = Continuous.([0.01, 0.011, 0.013, 0.016, 0.019, 0.021, 0.026, 0.03, 0.035, 0.037, 0.038, 0.04]) pack_maturities = [10e-5, 0.5, 1.0, 2.0, 3.0, 4.0, 5.0, 10.0, 15.0, 20.0, 25.0, 30.0] - c_param = Yields.NelsonSiegelCurve(0.04495841387198023, -0.03537510042719209, 0.0031561222355027227, 5.0) - c = Yields.Zero(NelsonSiegel(),pack_yields, pack_maturities) + c_param = Yield.NelsonSiegel(5.0, 0.04495841387198023, -0.03537510042719209, 0.0031561222355027227,) - @testset "zero rates: $t" for (t, r) in zip(pack_maturities, pack_yields) - @test Yields.zero(c, t) ≈ r atol = 0.003 + @testset "zero rates: $t" for (t, r) in zip(pack_maturities, pack_FinanceModels) + @test zero(c_param, t) ≈ r atol = 0.005 end end end @@ -55,47 +42,37 @@ @testset "NelsonSiegelSvensson" begin @testset "EURAAA_20191111" begin - c_zero = Yields.Zero(NelsonSiegelSvensson(),euraaa_zeros, euraaa_maturities) - c_par = Yields.Par(NelsonSiegelSvensson(),euraaa_pars, euraaa_maturities) + c_zero = fit(Yield.NelsonSiegelSvensson(), zqs) + c_par = c_zero #FinanceModels.Par(NelsonSiegelSvensson(), euraaa_pars, euraaa_maturities) - @testset "par and zero constructors" for c in [c_zero,c_par] + @testset "par and zero constructors" for c in [c_zero, c_par] @testset "zero rates: $t" for (t, r) in zip(euraaa_maturities, euraaa_zeros) - @test Yields.zero(c, t) ≈ r atol = 0.0001 + @test FinanceModels.zero(c, t) ≈ r atol = 0.005 + @test discount(c, t) ≈ discount(r, t) rtol = 0.0025 end @testset "par rates: $t" for (t, r) in zip(euraaa_maturities, euraaa_pars) # are the target rates on the ECB site continuous rates or periodic/bond-equivalent? - @test Yields.par(c, t) ≈ r atol = 0.0001 + @test FinanceModels.par(c, t) ≈ r atol = 0.005 end end - @testset "misc constructors" begin - for u in [Yields.Forward, Yields.Zero, Yields.Par] - @test u(NelsonSiegelSvensson(), euraaa_zeros) isa Yields.AbstractYieldCurve - end - end - - # test that rates as floats work - fzeros = Yields.rate.(euraaa_zeros) - c_fzero = Yields.Zero(NelsonSiegelSvensson(),fzeros, euraaa_maturities) - @test c_fzero == c_zero - end @testset "EURAAA_20191111 w parms given" begin - c = Yields.NelsonSiegelSvenssonCurve(0.629440 / 100, -1.218082 /100, 12.114098 /100, -14.181117 /100, 2.435976, 2.536963) + c = Yield.NelsonSiegelSvensson(2.435976, 2.536963, 0.629440 / 100, -1.218082 / 100, 12.114098 / 100, -14.181117 / 100) @testset "zero rates: $t" for (t, r) in zip(euraaa_maturities, euraaa_zeros) - @test Yields.zero(c, t) ≈ r atol = 0.0001 + @test FinanceModels.zero(c, t) ≈ r atol = 0.005 end @testset "par rates: $t" for (t, r) in zip(euraaa_maturities, euraaa_pars) # are the target rates on the ECB site continuous rates or periodic/bond-equivalent? - @test Yields.par(c, t) ≈ r atol = 0.0001 + @test FinanceModels.par(c, t) ≈ r atol = 0.005 end - @test Yields.zero(c,30) ≈ last(euraaa_zeros) atol = 0.0001 - @test discount(c,0) == 1.0 - @test discount(c,10) ≈ 1 / accumulation(c,10) + @test FinanceModels.zero(c, 30) ≈ last(euraaa_zeros) rtol = 0.0005 + @test discount(c, 0) == 1.0 + @test discount(c, 10) ≈ 1 / accumulation(c, 10) end end diff --git a/test/Project.toml b/test/Project.toml index 99573834..326322f5 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -1,8 +1,6 @@ [deps] +Accessors = "7d9f7c33-5ae7-4f3b-8dc6-eff91059b697" ActuaryUtilities = "bdd23359-8b1c-4f88-b89b-d11982a786f4" DecFP = "55939f99-70c6-5e9b-8bb0-5071ed7d61fd" +FinanceCore = "b9b1ffdd-6612-4b69-8227-7663be06e089" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" -Yields = "d7e99b2f-e7f3-4d9e-9f01-2338fc023ad3" - -[compat] -ActuaryUtilities = "^2,^3" diff --git a/test/RateCombination.jl b/test/RateCombination.jl deleted file mode 100644 index 9ada421f..00000000 --- a/test/RateCombination.jl +++ /dev/null @@ -1,50 +0,0 @@ -@testset "Rate Combinations" begin - riskfree_maturities = [0.5, 1.0, 1.5, 2.0] - riskfree = [5.0, 5.8, 6.4, 6.8] ./ 100 # spot rates - rf_curve = Yields.Zero(riskfree, riskfree_maturities) - - @testset "base + spread" begin - - spread_maturities = [0.5, 1.0, 1.5, 3.0] # different maturities - spread = [1.0, 1.8, 1.4, 1.8] ./ 100 # spot spreads - - spread_curve = Yields.Zero(spread, spread_maturities) - - yield = rf_curve + spread_curve - - @test rate(zero(yield, 0.5)) ≈ first(riskfree) + first(spread) - - @test discount(yield, 1.0) ≈ 1 / (1 + riskfree[2] + spread[2])^1 - @test discount(yield, 1.5) ≈ 1 / (1 + riskfree[3] + spread[3])^1.5 - end - - @testset "multiplicaiton and division" begin - @testset "multiplication" begin - factor = .79 - c = rf_curve * factor - (discount(c,10)-1 * factor) ≈ discount(rf_curve,10) - (accumulation(c,10)-1 * factor) ≈ accumulation(rf_curve,10) - forward(c,5,10) * factor ≈ forward(rf_curve,5,10) - Yields.par(c,10) * factor ≈ Yields.par(rf_curve,10) - - c = factor * rf_curve - (discount(c,10)-1 * factor) ≈ discount(rf_curve,10) - (accumulation(c,10)-1 * factor) ≈ accumulation(rf_curve,10) - forward(c,5,10) * factor ≈ forward(rf_curve,5,10) - Yields.par(c,10) * factor ≈ Yields.par(rf_curve,10) - - @test discount(Yields.Constant(0.1) * Yields.Constant(0.1),10) ≈ discount(Yields.Constant(0.01),10) - end - - @testset "division" begin - factor = .79 - c = rf_curve / (factor^-1) - (discount(c,10)-1 * factor) ≈ discount(rf_curve,10) - (accumulation(c,10)-1 * factor) ≈ accumulation(rf_curve,10) - forward(c,5,10) * factor ≈ forward(rf_curve,5,10) - Yields.par(c,10) * factor ≈ Yields.par(rf_curve,10) - @test discount(Yields.Constant(0.1) / Yields.Constant(0.5),10) ≈ discount(Yields.Constant(0.2),10) - @test discount(0.1 / Yields.Constant(0.5),10) ≈ discount(Yields.Constant(0.2),10) - end - end -end \ No newline at end of file diff --git a/test/SmithWilson.jl b/test/SmithWilson.jl index ffe6373f..b148d9cd 100644 --- a/test/SmithWilson.jl +++ b/test/SmithWilson.jl @@ -1,186 +1,140 @@ -@testset "SmithWilson" begin - - @testset "InstrumentQuotes" begin - - maturities = [1.3, 2.7] - prices = [1.1, 0.8] - zcq = Yields.ZeroCouponQuote.(prices, maturities) - @test isa(first(zcq), Yields.ZeroCouponQuote) - - @test_throws DimensionMismatch Yields.ZeroCouponQuote.([1.3, 2.4, 0.9], maturities) - - rates = [0.4, -0.7] - swq = Yields.SwapQuote.(rates, maturities, 3) - @test first(swq).frequency == 3 - @test_throws DimensionMismatch Yields.SwapQuote.([1.3, 2.4, 0.9], maturities, 3) - @test_throws DomainError Yields.SwapQuote.(rates, maturities, 0) - @test_throws DomainError Yields.SwapQuote.(rates, maturities, -2) +@testset "SmithWilson" begin - rates = [0.4, -0.7] - bbq = Yields.BulletBondQuote.(rates, prices, maturities, 3) - @test first(bbq).frequency == 3 - @test first(bbq).yield == first(rates) + ufr = 0.03 + α = 0.1 + u = [5.0, 7.0] + qb = [2.3, -1.2] + + # Basic behaviour + sw = Yield.SmithWilson(u, qb; ufr=ufr, α=α) + @test sw.ufr == ufr + @test sw.α == α + @test sw.u == u + @test sw.qb == qb + @test_throws DomainError Yield.SmithWilson(u, [2.4, -3.4, 8.9], ufr=ufr, α=α) + + # Empty u and Qb should result in a flat yield curve + # Use this to test methods expected from <:AbstractYieldCurve + # Only discount and zero are explicitly implemented, so the others should follow automatically + sw_flat = Yield.SmithWilson(Float64[], Float64[]; ufr=ufr, α=α) + @test discount(sw_flat, 10.0) == exp(-ufr * 10.0) + @test accumulation(sw_flat, 10.0) ≈ exp(ufr * 10.0) + @test zero(sw_flat, 8.0) ≈ Continuous(ufr) + @test discount.(sw_flat, [5.0, 10.0]) ≈ exp.(-ufr .* [5.0, 10.0]) + @test forward(sw_flat, 5.0, 8.0) ≈ Continuous(ufr) + + # A trivial Qb vector (=0) should result in a flat yield curve + ufr_curve = Yield.SmithWilson(u, [0.0, 0.0]; ufr=ufr, α=α) + @test discount(ufr_curve, 10.0) == exp(-ufr * 10.0) + + # A single payment at time 4, zero interest + curve_with_zero_yield = Yield.SmithWilson([4.0], reshape([1.0], 1, 1), [1.0]; ufr=ufr, α=α) + @test discount(curve_with_zero_yield, 4.0) == 1.0 + + # In the long end it's still just UFR + @test forward(curve_with_zero_yield, 1000.0, 2000.0) ≈ Continuous(ufr) + + # Three maturities have known discount factors + times = [1.0, 2.5, 5.6] + prices = [0.9, 0.7, 0.5] + qs = ZCBPrice.(prices, times) + + + + curve_three = fit(Yield.SmithWilson(ufr=ufr, α=α), qs) + @test [pv(curve_three, q.instrument) for q in qs] ≈ prices + + # Two cash flows with payments at three times + prices = [1.0, 0.9] + times = [1.0, 2.5, 5.6] + cfs = [0.1 0.1 + 1.1 0.1 + 0.0 1.1] + qs = [Quote(q[1], Cashflow.(q[2], times)) for q in zip(prices, eachcol(cfs))] + curve_nondiag = Yield.SmithWilson(times, cfs, prices; ufr=ufr, α=α) + @test transpose(cfs) * discount.(curve_nondiag, times) ≈ prices + curve_nondiag = fit(Yield.SmithWilson(ufr=ufr, α=α), qs) + @test transpose(cfs) * discount.(curve_nondiag, times) ≈ prices + + # Round-trip zero coupon quotes + zcq_times = [1.2, 4.5, 5.6] + zcq_prices = [1.0, 0.9, 1.2] + qs = ZCBPrice.(zcq_prices, zcq_times) + sw_zcq = fit(Yield.SmithWilson(ufr=ufr, α=α), qs) + @testset "ZeroCouponQuotes round-trip" for idx = 1:length(zcq_times) + @test discount(sw_zcq, zcq_times[idx]) ≈ zcq_prices[idx] + end + # Round-trip swap quotes + maturities = [1.2, 2.5, 3.6] + coupon = [-0.02, 0.3, 0.04] + frequency = Periodic.(2) + qs = Quote.( + ones(length(maturities)), + Bond.Fixed.(coupon, frequency, maturities) + ) + + sw_swq = fit(Yield.SmithWilson(ufr=ufr, α=α), qs) + swq_payments, swq_times = FinanceModels.cashflows_timepoints(qs) + @testset "SwapQuotes round-trip" for swapIdx = 1:length(coupon) + @test sum(discount.(sw_swq, swq_times) .* swq_payments[:, swapIdx]) ≈ 1.0 + end + @testset "SW ForwardStarting" begin + fwd_time = 1.0 + fwd = Yield.ForwardStarting(sw_swq, fwd_time) - @test_throws DimensionMismatch Yields.BulletBondQuote.([1.3, 2.4, 0.9], prices, maturities, 3) - @test_throws DimensionMismatch Yields.BulletBondQuote.(rates, prices, [4.3, 5.6, 4.4, 4.4], 3) - @test first(Yields.BulletBondQuote.(rates, [5.7], maturities, 3)).price == 5.7 - @test_throws DomainError Yields.BulletBondQuote.(rates, prices, maturities, 0) - @test_throws DomainError Yields.BulletBondQuote.(rates, prices, maturities, -4) + @test discount(fwd, 3.7) ≈ discount(sw_swq, fwd_time, fwd_time + 3.7) + end + # Round-trip bullet bond quotes (reuse data from swap quotes) + bbq_prices = [1.3, 0.1, 4.5] + qs = Quote.( + bbq_prices, + Bond.Fixed.(coupon, frequency, maturities) + ) + sw_bbq = fit(Yield.SmithWilson(ufr=ufr, α=α), qs) + @testset "BulletBondQuotes round-trip" for bondIdx = 1:length(bbq_prices) + @test sum(discount.(sw_bbq, swq_times) .* swq_payments[:, bondIdx]) ≈ bbq_prices[bondIdx] end - @testset "SmithWilson" begin - - ufr = 0.03 - α = 0.1 - u = [5.0, 7.0] - qb = [2.3, -1.2] - - # Basic behaviour - sw = Yields.SmithWilson(u, qb; ufr = ufr, α = α) - @test sw.ufr == ufr - @test sw.α == α - @test sw.u == u - @test sw.qb == qb - @test_throws DomainError Yields.SmithWilson(u, [2.4, -3.4, 8.9], ufr = ufr, α = α) - - # Empty u and Qb should result in a flat yield curve - # Use this to test methods expected from <:AbstractYieldCurve - # Only discount and zero are explicitly implemented, so the others should follow automatically - sw_flat = Yields.SmithWilson(Float64[], Float64[], ufr = ufr, α = α) - @test discount(sw_flat, 10.0) == exp(-ufr * 10.0) - @test accumulation(sw_flat, 10.0) ≈ exp(ufr * 10.0) - @test rate(convert(Yields.Continuous(), zero(sw_flat, 8.0))) ≈ ufr - @test discount.(sw_flat, [5.0, 10.0]) ≈ exp.(-ufr .* [5.0, 10.0]) - @test rate(convert(Yields.Continuous(), forward(sw_flat, 5.0, 8.0))) ≈ ufr - - # A trivial Qb vector (=0) should result in a flat yield curve - ufr_curve = Yields.SmithWilson(u, [0.0, 0.0], ufr = ufr, α = α) - @test discount(ufr_curve, 10.0) == exp(-ufr * 10.0) - - # A single payment at time 4, zero interest - curve_with_zero_yield = Yields.SmithWilson([4.0], reshape([1.0], 1, 1), [1.0], ufr = ufr, α = α) - @test discount(curve_with_zero_yield, 4.0) == 1.0 - - # In the long end it's still just UFR - @test rate(convert(Yields.Continuous(), forward(curve_with_zero_yield, 1000.0, 2000.0))) ≈ ufr - - # Three maturities have known discount factors - times = [1.0, 2.5, 5.6] - prices = [0.9, 0.7, 0.5] - cfs = [1 0 0 - 0 1 0 - 0 0 1] - - curve_three = Yields.SmithWilson(times, cfs, prices, ufr = ufr, α = α) - @test transpose(cfs) * discount.(curve_three, times) ≈ prices - - # Two cash flows with payments at three times - prices = [1.0, 0.9] - cfs = [0.1 0.1 - 1.0 0.1 - 0.0 1.0] - curve_nondiag = Yields.SmithWilson(times, cfs, prices, ufr = ufr, α = α) - @test transpose(cfs) * discount.(curve_nondiag, times) ≈ prices - - # Round-trip zero coupon quotes - zcq_times = [1.2, 4.5, 5.6] - zcq_prices = [1.0, 0.9, 1.2] - zcq = Yields.ZeroCouponQuote.(zcq_prices, zcq_times) - sw_zcq = Yields.SmithWilson(zcq, ufr = ufr, α = α) - @testset "ZeroCouponQuotes round-trip" for idx = 1:length(zcq_times) - @test discount(sw_zcq, zcq_times[idx]) ≈ zcq_prices[idx] - end - - # uneven frequencies - swq_maturities = [1.2, 2.5, 3.6] - swq_interests = [-0.02, 0.3, 0.04] - frequency = [2, 1, 2] - swq = Yields.SwapQuote.(swq_interests, swq_maturities, frequency) - swq_times = 0.5:0.5:3.5 # Maturities are rounded down to multiples of 1/frequency, [1.0, 2.5, 3.5] - swq_payments = [ - -0.01 0.3 0.02 - 0.99 0 0.02 - 0 0.3 0.02 - 0 0 0.02 - 0 1.3 0.02 - 0 0 0.02 - 0 0 1.02 - ] - - # Round-trip swap quotes - swq_maturities = [1.2, 2.5, 3.6] - swq_interests = [-0.02, 0.3, 0.04] - frequency = 2 - swq = Yields.SwapQuote.(swq_interests, swq_maturities, frequency) - swq_times = 0.5:0.5:3.5 # Maturities are rounded down to multiples of 1/frequency, [1.0, 2.5, 3.5] - swq_payments = [-0.01 0.15 0.02 - 0.99 0.15 0.02 - 0.0 0.15 0.02 - 0.0 0.15 0.02 - 0.0 1.15 0.02 - 0.0 0.0 0.02 - 0.0 0.0 1.02] - sw_swq = Yields.SmithWilson(swq, ufr = ufr, α = α) - @testset "SwapQuotes round-trip" for swapIdx = 1:length(swq_interests) - @test sum(discount.(sw_swq, swq_times) .* swq_payments[:, swapIdx]) ≈ 1.0 - end - - @test Yields.__ratetype(sw_swq) == Yields.Rate{Float64,typeof(Yields.DEFAULT_COMPOUNDING)} - - # Round-trip bullet bond quotes (reuse data from swap quotes) - bbq_prices = [1.3, 0.1, 4.5] - bbq = Yields.BulletBondQuote.(swq_interests, bbq_prices, swq_maturities, frequency) - sw_bbq = Yields.SmithWilson(bbq, ufr = ufr, α = α) - @testset "BulletBondQuotes round-trip" for bondIdx = 1:length(swq_interests) - @test sum(discount.(sw_bbq, swq_times) .* swq_payments[:, bondIdx]) ≈ bbq_prices[bondIdx] - end - - @testset "SW ForwardStarting" begin - fwd_time = 1.0 - fwd = Yields.ForwardStarting(sw_swq, fwd_time) - - @test discount(fwd, 3.7) ≈ discount(sw_swq, fwd_time, fwd_time + 3.7) - end - - # EIOPA risk free rate (no VA), 31 August 2021. - # https://www.eiopa.europa.eu/sites/default/files/risk_free_interest_rate/eiopa_rfr_20210831.zip - eiopa_output_qb = [-0.59556534586390800 - -0.07442224713453920 - -0.34193181987682400 - 1.54054875814153000 - -2.15552046042343000 - 0.73559290752221900 - 1.89365225129089000 - -2.75927773116240000 - 2.24893737130629000 - -1.51625404117395000 - 0.19284859623817400 - 1.13410725406271000 - 0.00153268224642171 - 0.00147942301778158 - -1.85022125156483000 - 0.00336230229850928 - 0.00324546553910162 - 0.00313268874430658 - 0.00302383083427276 - 1.36047951448615000] - eiopa_output_u = 1:20 - eiopa_ufr = log(1.036) - eiopa_α = 0.133394 - sw_eiopa_expected = Yields.SmithWilson(eiopa_output_u, eiopa_output_qb; ufr = eiopa_ufr, α = eiopa_α) - - eiopa_eurswap_maturities = [1:12; 15; 20] - eiopa_eurswap_rates = [-0.00615, -0.00575, -0.00535, -0.00485, -0.00425, -0.00375, -0.003145, - -0.00245, -0.00185, -0.00125, -0.000711, -0.00019, 0.00111, 0.00215] # Reverse engineered from output curve. This is the full precision of market quotes. - eiopa_eurswap_quotes = Yields.SwapQuote.(eiopa_eurswap_rates, eiopa_eurswap_maturities, 1) - sw_eiopa_actual = Yields.SmithWilson(eiopa_eurswap_quotes, ufr = eiopa_ufr, α = eiopa_α) - - @testset "Match EIOPA calculation" begin - @test sw_eiopa_expected.u ≈ sw_eiopa_actual.u - @test sw_eiopa_expected.qb ≈ sw_eiopa_actual.qb - end + + # EIOPA risk free rate (no VA), 31 August 2021. + # https://www.eiopa.europa.eu/sites/default/files/risk_free_interest_rate/eiopa_rfr_20210831.zip + eiopa_output_qb = [-0.59556534586390800 + -0.07442224713453920 + -0.34193181987682400 + 1.54054875814153000 + -2.15552046042343000 + 0.73559290752221900 + 1.89365225129089000 + -2.75927773116240000 + 2.24893737130629000 + -1.51625404117395000 + 0.19284859623817400 + 1.13410725406271000 + 0.00153268224642171 + 0.00147942301778158 + -1.85022125156483000 + 0.00336230229850928 + 0.00324546553910162 + 0.00313268874430658 + 0.00302383083427276 + 1.36047951448615000] + eiopa_output_u = 1:20 + eiopa_ufr = log(1.036) + eiopa_α = 0.133394 + sw_eiopa_expected = Yield.SmithWilson(eiopa_output_u, eiopa_output_qb; ufr=eiopa_ufr, α=eiopa_α) + + eiopa_eurswap_maturities = [1:12; 15; 20] + # Reverse engineered from output curve. This is the full precision of market quotes. + eiopa_eurswap_rates = [-0.00615, -0.00575, -0.00535, -0.00485, -0.00425, -0.00375, -0.003145, + -0.00245, -0.00185, -0.00125, -0.000711, -0.00019, 0.00111, 0.00215] + eiopa_eurswap_quotes = Quote.(1.0, Bond.Fixed.(eiopa_eurswap_rates, Periodic(1), eiopa_eurswap_maturities)) + sw_eiopa_actual = fit(Yield.SmithWilson(ufr=eiopa_ufr, α=eiopa_α), eiopa_eurswap_quotes) + + @testset "Match EIOPA calculation" begin + @test sw_eiopa_expected.u ≈ sw_eiopa_actual.u + @test sw_eiopa_expected.qb ≈ sw_eiopa_actual.qb end end \ No newline at end of file diff --git a/test/SmoothingSpline.jl b/test/SmoothingSpline.jl deleted file mode 100644 index 9e7ba905..00000000 --- a/test/SmoothingSpline.jl +++ /dev/null @@ -1,15 +0,0 @@ -@testset "SmoothingSpline" begin - - @testset "SmoothingSpline" begin - - # EURAAA_20191111 - @testset "EURAAA_20191111" begin - euraaa_yields = [-0.602, -0.6059, -0.6096, -0.613, -0.6215, -0.6279, -0.6341, -0.6327, -0.6106, -0.5694, -0.5161, -0.456, -0.3932, -0.3305, -0.2698, -0.2123, -0.1091, 0.0159, 0.0818, 0.1601, 0.2524, 0.315] - euraaa_maturities = [0.25, 0.333, 0.417, 0.5, 0.75, 1, 1.5, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12, 15, 17, 20, 25, 30] - ss_param = SmoothingSpline(0.6440719852424944, -1.2135915867261, -1.8281745438894712, 0.1) - ss = est_ss_params(euraaa_yields, euraaa_maturities) - @test ss = ss_param - end - end - -end diff --git a/test/Yield.jl b/test/Yield.jl new file mode 100644 index 00000000..009e2193 --- /dev/null +++ b/test/Yield.jl @@ -0,0 +1,329 @@ +@testset "constant curve and rate -> Constant" begin + yield = Yield.Constant(0.05) + + @test zero(yield, 1) ≈ Periodic(0.05, 1) + @test zero(Yield.Constant(Periodic(0.05, 2)), 10) ≈ Periodic(0.05, 2) + + @testset "constant discount time: $time" for time in [0, 0.5, 1, 10] + @test discount(yield, time) ≈ 1 / (1.05)^time + @test discount(yield, 0, time) ≈ 1 / (1.05)^time + @test discount(yield, 3, time + 3) ≈ 1 / (1.05)^time + end + @testset "constant discount scalar time: $time" for time in [0, 0.5, 1, 10] + @test discount(0.05, time) ≈ 1 / (1.05)^time + end + + @testset "constant accumulation scalar time: $time" for time in [0, 0.5, 1, 10] + @test accumulation(0.05, time) ≈ 1 * (1.05)^time + end + + @testset "broadcasting" begin + @test all(discount.(yield, [1, 2, 3]) .≈ 1 ./ 1.05 .^ (1:3)) + @test all(accumulation.(yield, [1, 2, 3]) .≈ 1.05 .^ (1:3)) + end + + @testset "constant accumulation time: $time" for time in [0, 0.5, 1, 10] + @test accumulation(yield, time) ≈ 1 * 1.05^time + @test accumulation(yield, 0, time) ≈ 1 * 1.05^time + @test accumulation(yield, 3, time + 3) ≈ 1 * 1.05^time + end + + + yield_2x = yield + yield + yield_add = yield + 0.05 + add_yield = 0.05 + yield + @testset "constant discount added" for time in [0, 0.5, 1, 10] + @test discount(yield_2x, time) ≈ 1 / (1.1)^time + @test discount(yield_add, time) ≈ 1 / (1.1)^time + @test discount(add_yield, time) ≈ 1 / (1.1)^time + end + + yield_1bps = yield - Yield.Constant(0.04) + yield_minus = yield - 0.01 + minus_yield = 0.05 - Yield.Constant(0.01) + @testset "constant discount subtraction" for time in [0, 0.5, 1, 10] + @test discount(yield_1bps, time) ≈ 1 / (1.01)^time + @test discount(yield_minus, time) ≈ 1 / (1.04)^time + @test discount(minus_yield, time) ≈ 1 / (1.04)^time + end +end + +@testset "bootstrapped class of curves" begin + + + + @testset "short curve" begin + zs = ZCBYield.([0.0, 0.05], [1, 2]) + z = fit(Spline.Cubic(), zs, Fit.Bootstrap()) + + @test zero(z, 1) ≈ Periodic(0.00, 1) + @test discount(z, 1) ≈ 1.00 + @test zero(z, 2) ≈ Periodic(0.05, 1) + + # test no times constructor + zs = ZCBYield([0.0, 0.05]) + z = fit(Spline.Cubic(), zs, Fit.Bootstrap()) + @test zero(z, 1) ≈ Periodic(0.00, 1) + @test discount(z, 1) ≈ 1.00 + @test zero(z, 2) ≈ Periodic(0.05, 1) + end + + @testset "Salomon Understanding the Yield Curve Pt 1 Figure 9" begin + maturity = collect(1:10) + + par = [6.0, 8.0, 9.5, 10.5, 11.0, 11.25, 11.38, 11.44, 11.48, 11.5] ./ 100 + spot = [6.0, 8.08, 9.72, 10.86, 11.44, 11.71, 11.83, 11.88, 11.89, 11.89] ./ 100 + + # the forwards for 7+ have been adjusted from the table - perhaps rounding issues are exacerbated + # in the text? forwards for <= 6 matched so reasonably confident that the algo is correct + # fwd = [6.,10.2,13.07,14.36,13.77,13.1,12.55,12.2,11.97,11.93] ./ 100 # from text + fwd = [6.0, 10.2, 13.07, 14.36, 13.77, 13.1, 12.61, 12.14, 12.05, 11.84] ./ 100 # modified + + rs = FinanceModels.ParYield.(Periodic(1).(par), maturity) + m = fit(Spline.Cubic(), rs, Fit.Bootstrap()) + @testset "quadratic UTYC Figure 9 par -> spot : $mat" for mat in maturity + @test zero(m, mat) ≈ Periodic(spot[mat], 1) atol = 0.0001 + @test forward(m, mat - 1) ≈ FinanceModels.Periodic(fwd[mat], 1) atol = 0.0001 + end + + m = fit(Spline.Linear(), rs, Fit.Bootstrap()) + + @testset "linear UTYC Figure 9 par -> spot : $mat" for mat in maturity + @test zero(m, mat) ≈ Periodic(spot[mat], 1) atol = 0.0001 + @test forward(m, mat - 1) ≈ FinanceModels.Periodic(fwd[mat], 1) atol = 0.0001 + end + + end + + @testset "Hull" begin + # Par Yield, pg 85 + + c = ParYield.(FinanceModels.Periodic.([0.0687, 0.0687], 2), [2, 3]) + + m = fit(Spline.Linear(), c, Fit.Bootstrap()) + @test FinanceModels.par(m, 2) ≈ FinanceModels.Periodic(0.0687, 2) atol = 0.00001 + + end + + @testset "simple rate and forward" begin + # Risk Managment and Financial Institutions, 5th ed. Appendix B + + maturity = [0.5, 1.0, 1.5, 2.0] + zero = [5.0, 5.8, 6.4, 6.8] ./ 100 + zs = ZCBYield.(zero, maturity) + @testset "$i" for (i, curve) in enumerate([fit(Spline.Cubic(), zs, Fit.Bootstrap()), fit(Spline.Linear(), zs, Fit.Bootstrap()), fit(Spline.Quadratic(), zs, Fit.Bootstrap()), fit(Spline.BSpline(5), zs, Fit.Bootstrap())]) + + @test discount(curve, 1) ≈ 1 / 1.058 + @test discount(curve, 1.5) ≈ 1 / 1.064^1.5 + @test discount(curve, 2) ≈ 1 / 1.068^2 + + @test discount(curve, 0) ≈ 1 + @test discount(curve, 1) ≈ 1 / (1 + zero[2]) + @test discount(curve, 2) ≈ 1 / (1 + zero[4])^2 + + @test forward(curve, 0.5, 1.0) ≈ FinanceModels.Periodic(6.6 / 100, 1) atol = 0.001 + @test forward(curve, 1.0, 1.5) ≈ FinanceModels.Periodic(7.6 / 100, 1) atol = 0.001 + @test forward(curve, 1.5, 2.0) ≈ FinanceModels.Periodic(8.0 / 100, 1) atol = 0.001 + @testset "broadcasting" begin + @test all(discount.(curve, [1, 2]) .≈ [1 / 1.058, 1 / 1.068^2]) + @test all(accumulation.(curve, [1, 2]) .≈ [1.058, 1.068^2]) + end + end + + end + + @testset "Forward Rates" begin + # Risk Managment and Financial Institutions, 5th ed. Appendix B + forwards = [0.05, 0.04, 0.03, 0.08] + qs = ForwardYields(forwards, [1, 2, 3, 4]) + curve = fit(Spline.Cubic(), qs, Fit.Bootstrap()) + + + @testset "discounts: $t" for (t, r) in enumerate(forwards) + @test discount(curve, t) ≈ reduce((v, r) -> v / (1 + r), forwards[1:t]; init=1.0) + end + + # test from ActuaryUtilities + + @testset "pv with vector discount rates" begin + cf = [100, 100] + f(rates) = fit(Spline.Linear(), ForwardYields(rates), Fit.Bootstrap()) + f(rates, times) = fit(Spline.Linear(), ForwardYields(rates, times), Fit.Bootstrap()) + @test pv(f([0.0, 0.05]), cf) ≈ 100 / 1.0 + 100 / 1.05 + @test pv(f([0.0, 0.05]), cf) ≈ 100 / 1.0 + 100 / 1.05 + @test pv(f([0.05, 0.0]), cf) ≈ 100 / 1.05 + 100 / 1.05 + @test pv(f([0.05, 0.1]), cf) ≈ 100 / 1.05 + 100 / 1.05 / 1.1 + + ts = [0.5, 1] + @test pv(f([0.0, 0.05], ts), cf, ts) ≈ 100 / 1.0 + 100 / 1.05^0.5 + @test pv(f([0.05, 0.0], ts), cf, ts) ≈ 100 / 1.05^0.5 + 100 / 1.05^0.5 + @test pv(f([0.05, 0.1], ts), cf, ts) ≈ 100 / 1.05^0.5 + 100 / (1.05^0.5) / (1.1^0.5) + + end + + + # test constructor without times + qs = ForwardYields(forwards) + + @testset "discounts: $t" for (t, r) in enumerate(forwards) + @test discount(curve, t) ≈ reduce((v, r) -> v / (1 + r), forwards[1:t]; init=1.0) + end + + @test accumulation(curve, 0, 1) ≈ 1.05 + @test accumulation(curve, 1, 2) ≈ 1.04 + @test accumulation(curve, 0, 2) ≈ 1.04 * 1.05 + + @testset "broadcasting" begin + @test all(accumulation.(curve, [1, 2]) .≈ [1.05, 1.04 * 1.05]) + @test all(discount.(curve, [1, 2]) .≈ 1 ./ [1.05, 1.04 * 1.05]) + end + + # test construction using vector of reals and of Rates + curve_c = let + qs = ForwardYields(Continuous.(forwards), [1, 2, 3, 4]) + fit(Spline.Cubic(), qs, Fit.Bootstrap()) + end + @test discount(curve, 1) > discount(curve_c, 1) + + + # addition / subtraction + @test discount(curve + 0.1, 1) ≈ 1 / 1.15 + @test discount(curve - 0.03, 1) ≈ 1 / 1.02 + + + + @testset "with specified non integer timepoints" begin + i = [0.0, 0.05] + times = [0.5, 1.5] + qs = ForwardYields(i, times) + m = fit(Spline.Linear(), qs, Fit.Bootstrap()) + @test discount(m, 0.5) ≈ 1 / 1.0^0.5 + @test discount(m, 1.5) ≈ 1 / 1.0^0.5 / 1.05^1 + + end + + end + + @testset "forwardcurve" begin + maturity = [0.5, 1.0, 1.5, 2.0] + zeros = [5.0, 5.8, 6.4, 6.8] ./ 100 + qs = ZCBYield.(zeros, maturity) + curve = fit(Spline.Cubic(), qs, Fit.Bootstrap()) + + fwd = Yield.ForwardStarting(curve, 1.0) + @test discount(fwd, 0) ≈ 1 + @test discount(fwd, 0.5) ≈ discount(curve, 1, 1.5) + @test discount(fwd, 1) ≈ discount(curve, 1, 2) + @test accumulation(fwd, 1) ≈ accumulation(curve, 1, 2) + + @test zero(fwd, 1) ≈ forward(curve, 1, 2) + end + + + + @testset "actual cmt treasury" begin + # Fabozzi 5-5,5-6 + cmt = [5.25, 5.5, 5.75, 6.0, 6.25, 6.5, 6.75, 6.8, 7.0, 7.1, 7.15, 7.2, 7.3, 7.35, 7.4, 7.5, 7.6, 7.6, 7.7, 7.8] ./ 100 + mats = collect(0.5:0.5:10.0) + qs = CMTYield.(cmt, mats) + target_raw = [5.25, 5.5, 5.76, 6.02, 6.28, 6.55, 6.82, 6.87, 7.09, 7.2, 7.26, 7.31, 7.43, 7.48, 7.54, 7.67, 7.8, 7.79, 7.93, 8.07] ./ 100 + targets = Periodic(2).(target_raw) + targets[1:2] .= Periodic(1)(target_raw[1:2]) # 1 year is a no-coupon, BEY yield, the rest are semiannual BEY + + curves = [ + fit(Spline.Linear(), qs, Fit.Bootstrap()), + fit(Spline.Quadratic(), qs, Fit.Bootstrap()), + fit(Spline.Cubic(), qs, Fit.Bootstrap()), + ] + + @testset "curve bootstrapping choices" for curve in curves + @testset "Fabozzi bootstrapped rates" for (r, mat, target) in zip(cmt, mats, targets) + @test zero(curve, mat) ≈ target atol = 0.0001 + end + end + + # Hull, problem 4.34 + adj = ((1 + 0.051813 / 2)^2 - 1) * 100 + cmt = [4.0816, adj, 5.4986, 5.8620] ./ 100 + mats = [0.5, 1.0, 1.5, 2.0] + curve = fit(Spline.Linear(), CMTYield.(cmt, mats), Fit.Bootstrap()) + targets = Continuous.([4.0405, 5.1293, 5.4429, 5.8085] ./ 100) + @testset "Hull bootstrapped rates" for (r, mat, target) in zip(cmt, mats, targets) + @test zero(curve, mat) ≈ target atol = 0.001 + end + + # test that showing the curve doesn't error + @test length(repr(curve)) > 0 + + # # https://www.federalreserve.gov/pubs/feds/2006/200628/200628abs.html + # # 2020-04-02 data + # cmt = [0.0945,0.2053,0.4431,0.7139,0.9724,1.2002,1.3925,1.5512,1.6805,1.7853,1.8704,1.9399,1.9972,2.045,2.0855,2.1203,2.1509,2.1783,2.2031,2.2261,2.2477,2.2683,2.2881,2.3074,2.3262,2.3447,2.3629,2.3809,2.3987,2.4164] ./ 100 + # mats = collect(1:30) + # curve = FinanceModels.USCMT(cmt,mats) + # target = [0.0945,0.2053,0.444,0.7172,0.9802,1.2142,1.4137,1.5797,1.7161,1.8275,1.9183,1.9928,2.0543,2.1056,2.1492,2.1868,2.2198,2.2495,2.2767,2.302,2.3261,2.3494,2.372,2.3944,2.4167,2.439,2.4614,2.4839,2.5067,2.5297] ./ 100 + + # @testset "FRB data" for (t,mat,target) in zip(1:length(mats),mats,target) + # @show mat + # if mat >= 1 + # @test rate(zero(curve,mat, FinanceModels.Continuous())) ≈ target[mat] atol=0.001 + # end + # end + end + + @testset "OIS" begin + ois = [1.8, 2.0, 2.2, 2.5, 3.0, 4.0] ./ 100 + mats = [1 / 12, 1 / 4, 1 / 2, 1, 2, 5] + qs = OISYield.(ois, mats) + targets = Continuous.([0.017987, 0.019950, 0.021880, 0.024693, 0.029994, 0.040401]) + + curve = fit(Spline.Linear(), qs, Fit.Bootstrap()) + @testset "bootstrapped rates" for (mat, target) in zip(mats, targets) + @test zero(curve, mat) ≈ target atol = 0.001 + end + curve = fit(Spline.Cubic(), qs, Fit.Bootstrap()) + @testset "bootstrapped rates" for (mat, target) in zip(mats, targets) + @test zero(curve, mat) ≈ target atol = 0.001 + end + end + + @testset "par" begin + @testset "first payment logic" begin + ct = Bond.coupon_times + @test ct(0.5, 1) ≈ 0.5:1:0.5 + @test ct(1.5, 1) ≈ 0.5:1:1.5 + @test ct(0.75, 1) ≈ 0.75:1:0.75 + @test ct(1, 1) ≈ 1:1:1 + @test ct(1, 2) ≈ 0.5:0.5:1.0 + @test ct(0.5, 2) ≈ 0.5:0.5:0.5 + @test ct(1.5, 2) ≈ 0.5:0.5:1.5 + end + + # https://quant.stackexchange.com/questions/57608/how-to-compute-par-yield-from-zero-rate-curve + c = fit(Spline.Cubic(), ZCBYield.(Continuous.([0.02, 0.025, 0.03, 0.035]), 0.5:0.5:2), Fit.Bootstrap()) + @test FinanceModels.par(c, 2) ≈ Periodic(0.03508591, 2) atol = 0.000001 + + c = Yield.Constant(0.04) + @testset "misc combinations" for t in 0.5:0.5:5 + @test FinanceModels.par(c, t; frequency=1) ≈ FinanceModels.Periodic(0.04, 1) + @test FinanceModels.par(c, t) ≈ FinanceModels.Periodic(0.04, 1) + @test FinanceModels.par(c, t, frequency=4) ≈ FinanceModels.Periodic(0.04, 1) + end + + @test FinanceModels.par(c, 0.6) ≈ FinanceModels.Periodic(0.04, 1) + + @testset "round trip" begin + maturity = collect(1:10) + + pars = [6.0, 8.0, 9.5, 10.5, 11.0, 11.25, 11.38, 11.44, 11.48, 11.5] ./ 100 + + curve = fit(Spline.Cubic(), ParYield.(pars, maturity), Fit.Bootstrap()) + + for (p, m) in zip(pars, maturity) + @test par(curve, m) ≈ Periodic(p, 2) atol = 0.001 + end + end + + + end + +end diff --git a/test/bootstrap.jl b/test/bootstrap.jl deleted file mode 100644 index 97b88390..00000000 --- a/test/bootstrap.jl +++ /dev/null @@ -1,331 +0,0 @@ -@testset "bootstrapped class of curves" begin - - - @testset "constant curve and rate -> Constant" begin - yield = Yields.Constant(0.05) - rate = Yields.Yields.Rate(0.05, Yields.Periodic(1)) - - @test Yields.zero(yield, 1) == Yields.Rate(0.05, Yields.Periodic(1)) - @test Yields.zero(Yields.Constant(Yields.Periodic(0.05,2)), 10) == Yields.Rate(0.05, Yields.Periodic(2)) - @test Yields.zero(yield, 5, Yields.Periodic(2)) == convert(Yields.Periodic(2), Yields.Rate(0.05, Yields.Periodic(1))) - - @testset "constant discount time: $time" for time in [0, 0.5, 1, 10] - @test discount(yield, time) ≈ 1 / (1.05)^time - @test discount(yield, 0, time) ≈ 1 / (1.05)^time - @test discount(yield, 3, time + 3) ≈ 1 / (1.05)^time - end - @testset "constant discount scalar time: $time" for time in [0, 0.5, 1, 10] - @test discount(0.05, time) ≈ 1 / (1.05)^time - end - - @testset "constant accumulation scalar time: $time" for time in [0, 0.5, 1, 10] - @test accumulation(0.05, time) ≈ 1 * (1.05)^time - end - - @testset "broadcasting" begin - @test all(discount.(yield, [1, 2, 3]) .≈ 1 ./ 1.05 .^ (1:3)) - @test all(accumulation.(yield, [1, 2, 3]) .≈ 1.05 .^ (1:3)) - end - - @testset "constant accumulation time: $time" for time in [0, 0.5, 1, 10] - @test accumulation(yield, time) ≈ 1 * 1.05^time - @test accumulation(yield, 0, time) ≈ 1 * 1.05^time - @test accumulation(yield, 3, time + 3) ≈ 1 * 1.05^time - end - - - yield_2x = yield + yield - yield_add = yield + 0.05 - add_yield = 0.05 + yield - @testset "constant discount added" for time in [0, 0.5, 1, 10] - @test discount(yield_2x, time) ≈ 1 / (1.1)^time - @test discount(yield_add, time) ≈ 1 / (1.1)^time - @test discount(add_yield, time) ≈ 1 / (1.1)^time - end - - yield_1bps = yield - Yields.Constant(0.04) - yield_minus = yield - 0.01 - minus_yield = 0.05 - Yields.Constant(0.01) - @testset "constant discount subtraction" for time in [0, 0.5, 1, 10] - @test discount(yield_1bps, time) ≈ 1 / (1.01)^time - @test discount(yield_minus, time) ≈ 1 / (1.04)^time - @test discount(minus_yield, time) ≈ 1 / (1.04)^time - end - end - - @testset "short curve" begin - z = Yields.Zero([0.0, 0.05], [1, 2]) - @test zero(z, 1) ≈ Periodic(0.00,1) - @test discount(z, 1) ≈ 1.00 - @test zero(z, 2) ≈ Periodic(0.05,1) - - # test no times constructor - z = Yields.Zero([0.0, 0.05]) - @test zero(z, 1) ≈ Periodic(0.00,1) - @test discount(z, 1) ≈ 1.00 - @test zero(z, 2) ≈ Periodic(0.05,1) - end - - @testset "Step curve" begin - y = Yields.Step([0.02, 0.05], [1, 2]) - - @test zero(y, 0.5) ≈ Periodic(0.02,1) - - @test discount(y, 0.0) ≈ 1 - @test discount(y, 0.5) ≈ 1 / (1.02)^(0.5) - @test discount(y, 1) ≈ 1 / (1.02)^(1) - @test discount(y, 10) ≈ 1 / (1.02)^(1) / (1.05)^(9) - @test zero(y, 1) ≈ Periodic(0.02,1) - - @test discount(y, 2) ≈ 1 / (1.02) / 1.05 - - @test discount(y, 2) ≈ 1 / (1.02) / 1.05 - - @test discount(y, 1.5) ≈ 1 / (1.02) / 1.05^(0.5) - - @testset "broadcasting" begin - @test all(discount.(y, [1, 2]) .== [1 / 1.02, 1 / 1.02 / 1.05]) - @test all(accumulation.(y, [1, 2]) .== [1.02, 1.02 * 1.05]) - end - - y = Yields.Step([0.02, 0.07]) - - @test zero(y, 0.5) ≈ Periodic(0.02,1) - @test zero(y, 1) ≈ Periodic(0.02,1) - @test zero(y, 1.5) ≈ Periodic(accumulation(y,1.5)^(1/1.5)-1,1) - - end - - @testset "Salomon Understanding the Yield Curve Pt 1 Figure 9" begin - maturity = collect(1:10) - - par = [6.0, 8.0, 9.5, 10.5, 11.0, 11.25, 11.38, 11.44, 11.48, 11.5] ./ 100 - spot = [6.0, 8.08, 9.72, 10.86, 11.44, 11.71, 11.83, 11.88, 11.89, 11.89] ./ 100 - - # the forwards for 7+ have been adjusted from the table - perhaps rounding issues are exacerbated - # in the text? forwards for <= 6 matched so reasonably confident that the algo is correct - # fwd = [6.,10.2,13.07,14.36,13.77,13.1,12.55,12.2,11.97,11.93] ./ 100 # from text - fwd = [6.0, 10.2, 13.07, 14.36, 13.77, 13.1, 12.61, 12.14, 12.05, 11.84] ./ 100 # modified - - y = Yields.Par(Periodic(1).(par), maturity) - @testset "quadratic UTYC Figure 9 par -> spot : $mat" for mat in maturity - @test zero(y, mat) ≈ Periodic(spot[mat],1) atol = 0.0001 - @test forward(y, mat - 1) ≈ Yields.Periodic(fwd[mat], 1) atol = 0.0001 - end - - y = Yields.Par(Bootstrap(LinearSpline()),Yields.Rate.(par, Yields.Periodic(1)), maturity) - - @testset "linear UTYC Figure 9 par -> spot : $mat" for mat in maturity - @test zero(y, mat) ≈ Periodic(spot[mat],1) atol = 0.0001 - @test forward(y, mat - 1) ≈ Yields.Periodic(fwd[mat], 1) atol = 0.0001 - end - - end - - @testset "Hull" begin - # Par Yield, pg 85 - - c = Yields.Par(Yields.Periodic.([0.0687,0.0687],2), [2,3]) - - @test Yields.par(c,2) ≈ Yields.Periodic(0.0687,2) atol = 0.00001 - - end - - @testset "simple rate and forward" begin - # Risk Managment and Financial Institutions, 5th ed. Appendix B - - maturity = [0.5, 1.0, 1.5, 2.0] - zero = [5.0, 5.8, 6.4, 6.8] ./ 100 - curve = Yields.Zero(zero, maturity) - - for curve in [Yields.Zero(zero, maturity),Yields.Zero(Bootstrap(LinearSpline()),zero, maturity),Yields.Zero(Bootstrap(QuadraticSpline()),zero, maturity)] - @test discount(curve, 0) ≈ 1 - @test discount(curve, 1) ≈ 1 / (1 + zero[2]) - @test discount(curve, 2) ≈ 1 / (1 + zero[4])^2 - - @test forward(curve, 0.5, 1.0) ≈ Yields.Periodic(6.6 / 100, 1) atol = 0.001 - @test forward(curve, 1.0, 1.5) ≈ Yields.Periodic(7.6 / 100, 1) atol = 0.001 - @test forward(curve, 1.5, 2.0) ≈ Yields.Periodic(8.0 / 100, 1) atol = 0.001 - end - - y = Yields.Zero(zero) - - @test discount(y, 1) ≈ 1 / 1.05 - @test discount(y, 2) ≈ 1 / 1.058^2 - - @testset "broadcasting" begin - @test all(discount.(y, [1, 2]) .≈ [1 / 1.05, 1 / 1.058^2]) - @test all(accumulation.(y, [1, 2]) .≈ [1.05, 1.058^2]) - end - - end - - @testset "Forward Rates" begin - # Risk Managment and Financial Institutions, 5th ed. Appendix B - - forwards = [0.05, 0.04, 0.03, 0.08] - curve = Yields.Forward(forwards, [1, 2, 3, 4]) - - - @testset "discounts: $t" for (t, r) in enumerate(forwards) - @test discount(curve, t) ≈ reduce((v, r) -> v / (1 + r), forwards[1:t]; init = 1.0) - end - - # test constructor without times - curve = Yields.Forward(forwards) - - @testset "discounts: $t" for (t, r) in enumerate(forwards) - @test discount(curve, t) ≈ reduce((v, r) -> v / (1 + r), forwards[1:t]; init = 1.0) - end - - @test accumulation(curve, 0, 1) ≈ 1.05 - @test accumulation(curve, 1, 2) ≈ 1.04 - @test accumulation(curve, 0, 2) ≈ 1.04 * 1.05 - - # test construction using vector of reals and of Rates - @test discount(Yields.Forward(forwards), 1) > discount(Yields.Forward(Yields.Continuous.(forwards)), 1) - - @testset "broadcasting" begin - @test all(accumulation.(curve, [1, 2]) .≈ [1.05, 1.04 * 1.05]) - @test all(discount.(curve, [1, 2]) .≈ 1 ./ [1.05, 1.04 * 1.05]) - end - - # addition / subtraction - @test discount(curve + 0.1, 1) ≈ 1 / 1.15 - @test discount(curve - 0.03, 1) ≈ 1 / 1.02 - - - - @testset "with specified timepoints" begin - i = [0.0, 0.05] - times = [0.5, 1.5] - y = Yields.Forward(i, times) - @test discount(y, 0.5) ≈ 1 / 1.0^0.5 - @test discount(y, 1.5) ≈ 1 / 1.0^0.5 / 1.05^1 - - end - - end - - @testset "forwardcurve" begin - maturity = [0.5, 1.0, 1.5, 2.0] - zeros = [5.0, 5.8, 6.4, 6.8] ./ 100 - curve = Yields.Zero(zeros, maturity) - - fwd = Yields.ForwardStarting(curve, 1.0) - @test discount(fwd, 0) ≈ 1 - @test discount(fwd, 0.5) ≈ discount(curve, 1, 1.5) - @test discount(fwd, 1) ≈ discount(curve, 1, 2) - @test accumulation(fwd, 1) ≈ accumulation(curve, 1, 2) - - @test zero(fwd,1) ≈ forward(curve,1,2) - @test zero(fwd,1,Yields.Continuous()) ≈ convert(Yields.Continuous(),forward(curve,1,2)) - end - - - - @testset "actual cmt treasury" begin - # Fabozzi 5-5,5-6 - cmt = [5.25, 5.5, 5.75, 6.0, 6.25, 6.5, 6.75, 6.8, 7.0, 7.1, 7.15, 7.2, 7.3, 7.35, 7.4, 7.5, 7.6, 7.6, 7.7, 7.8] ./ 100 - mats = collect(0.5:0.5:10.0) - targets = [5.25, 5.5, 5.76, 6.02, 6.28, 6.55, 6.82, 6.87, 7.09, 7.2, 7.26, 7.31, 7.43, 7.48, 7.54, 7.67, 7.8, 7.79, 7.93, 8.07] ./ 100 - target_periodicity = fill(2, length(mats)) - target_periodicity[2] = 1 # 1 year is a no-coupon, BEY yield, the rest are semiannual BEY - @testset "curve bootstrapping choices" for curve in [Yields.CMT(cmt, mats), Yields.CMT(Bootstrap(LinearSpline()),cmt, mats), Yields.CMT(Bootstrap(QuadraticSpline()),cmt, mats)] - @testset "Fabozzi bootstrapped rates" for (r, mat, target, tp) in zip(cmt, mats, targets, target_periodicity) - @test rate(zero(curve, mat, Yields.Periodic(tp))) ≈ target atol = 0.0001 - end - - @testset "Fabozzi bootstrapped rates" for (r, mat, target, tp) in zip(cmt, mats, targets, target_periodicity) - @test rate(zero(curve, mat, Yields.Periodic(tp))) ≈ target atol = 0.0001 - end - - @testset "Fabozzi bootstrapped rates" for (r, mat, target, tp) in zip(cmt, mats, targets, target_periodicity) - @test rate(zero(curve, mat, Yields.Periodic(tp))) ≈ target atol = 0.0001 - end - end - - # Hull, problem 4.34 - adj = ((1 + 0.051813 / 2)^2 - 1) * 100 - cmt = [4.0816, adj, 5.4986, 5.8620] ./ 100 - mats = [0.5, 1.0, 1.5, 2.0] - curve = Yields.CMT(cmt, mats) - targets = [4.0405, 5.1293, 5.4429, 5.8085] ./ 100 - @testset "Hull bootstrapped rates" for (r, mat, target) in zip(cmt, mats, targets) - @test rate(zero(curve, mat, Yields.Continuous())) ≈ target atol = 0.001 - end - - # test that showing the curve doesn't error - @test length(repr(curve)) > 0 - - # # https://www.federalreserve.gov/pubs/feds/2006/200628/200628abs.html - # # 2020-04-02 data - # cmt = [0.0945,0.2053,0.4431,0.7139,0.9724,1.2002,1.3925,1.5512,1.6805,1.7853,1.8704,1.9399,1.9972,2.045,2.0855,2.1203,2.1509,2.1783,2.2031,2.2261,2.2477,2.2683,2.2881,2.3074,2.3262,2.3447,2.3629,2.3809,2.3987,2.4164] ./ 100 - # mats = collect(1:30) - # curve = Yields.USCMT(cmt,mats) - # target = [0.0945,0.2053,0.444,0.7172,0.9802,1.2142,1.4137,1.5797,1.7161,1.8275,1.9183,1.9928,2.0543,2.1056,2.1492,2.1868,2.2198,2.2495,2.2767,2.302,2.3261,2.3494,2.372,2.3944,2.4167,2.439,2.4614,2.4839,2.5067,2.5297] ./ 100 - - # @testset "FRB data" for (t,mat,target) in zip(1:length(mats),mats,target) - # @show mat - # if mat >= 1 - # @test rate(zero(curve,mat, Yields.Continuous())) ≈ target[mat] atol=0.001 - # end - # end - end - - @testset "OIS" begin - ois = [1.8, 2.0, 2.2, 2.5, 3.0, 4.0] ./ 100 - mats = [1 / 12, 1 / 4, 1 / 2, 1, 2, 5] - targets = [0.017987, 0.019950, 0.021880, 0.024693, 0.029994, 0.040401] - - curve = Yields.OIS(ois, mats) - @testset "bootstrapped rates" for (r, mat, target) in zip(ois, mats, targets) - @test rate(zero(curve, mat, Yields.Continuous())) ≈ target atol = 0.001 - end - curve = Yields.OIS(Bootstrap(LinearSpline()),ois, mats) - @testset "bootstrapped rates" for (r, mat, target) in zip(ois, mats, targets) - @test rate(zero(curve, mat, Yields.Continuous())) ≈ target atol = 0.001 - end - end - - @testset "par" begin - @testset "first payment logic" begin - ct = Yields.coupon_times - @test ct(0.5,1) ≈ 0.5:1:0.5 - @test ct(1.5,1) ≈ 0.5:1:1.5 - @test ct(0.75,1) ≈ 0.75:1:0.75 - @test ct(1,1) ≈ 1:1:1 - @test ct(1,2) ≈ 0.5:0.5:1.0 - @test ct(0.5,2) ≈ 0.5:0.5:0.5 - @test ct(1.5,2) ≈ 0.5:0.5:1.5 - end - - # https://quant.stackexchange.com/questions/57608/how-to-compute-par-yield-from-zero-rate-curve - c = Yields.Zero(Yields.Continuous.([0.02,0.025,0.03,0.035]),0.5:0.5:2) - @test Yields.par(c,2) ≈ Yields.Periodic(0.03508591,2) atol = 0.000001 - - c = Yields.Constant(0.04) - @testset "misc combinations" for t in 0.5:0.5:5 - @test Yields.par(c,t;frequency=1) ≈ Yields.Periodic(0.04,1) - @test Yields.par(c,t) ≈ Yields.Periodic(0.04,1) - @test Yields.par(c,t,frequency=4) ≈ Yields.Periodic(0.04,1) - end - - @test Yields.par(c,0.6) ≈ Yields.Periodic(0.04,1) - - @testset "round trip" begin - maturity = collect(1:10) - - par = [6.0, 8.0, 9.5, 10.5, 11.0, 11.25, 11.38, 11.44, 11.48, 11.5] ./ 100 - - curve = Yields.Par(par,maturity) - - for (p,m) in zip(par,maturity) - @test Yields.par(curve,m) ≈ Yields.Periodic(p,2) atol = 0.001 - end - end - - - end - -end diff --git a/test/generic.jl b/test/generic.jl index f9aff46b..007d55ae 100644 --- a/test/generic.jl +++ b/test/generic.jl @@ -1,24 +1,22 @@ # test extending type and that various generic methods are defined # to fulfill the API of AbstractYieldCurve @testset "generic methods and type extensions" begin - struct MyYield <: Yields.AbstractYieldCurve + struct MyYield <: Yield.AbstractYieldModel rate end - Yields.discount(c::MyYield,to) = exp(-c.rate * to) - # Base.zero(c::MyYield,to) = Continuous(c.rate) + FinanceCore.discount(c::MyYield, to) = exp(-c.rate * to) + Base.zero(c::MyYield, to) = Continuous(c.rate) my = MyYield(0.05) - @test zero(my,1) ≈ Continuous(0.05) - @test Yields.forward(my,1) ≈ Continuous(0.05) - @test Yields.forward(my,1,2) ≈ Continuous(0.05) - @test discount(my,1,2) ≈ exp(-0.05*1) - @test accumulation(my,1,2) ≈ exp(0.05*1) - @test accumulation(my,1) ≈ exp(0.05*1) - @test Yields.__ratetype(my) == Yields.Rate{Float64,Continuous} - @test Yields.FinanceCore.CompoundingFrequency(my) == Continuous() + @test zero(my, 1) ≈ Continuous(0.05) + @test forward(my, 1) ≈ Continuous(0.05) + @test forward(my, 1, 2) ≈ Continuous(0.05) + @test discount(my, 1, 2) ≈ exp(-0.05 * 1) + @test accumulation(my, 1, 2) ≈ exp(0.05 * 1) + @test accumulation(my, 1) ≈ exp(0.05 * 1) - @test Yields.par(my,1) |> Yields.rate > 0 + @test FinanceModels.par(my, 1) |> FinanceModels.rate > 0 end \ No newline at end of file diff --git a/test/misc.jl b/test/misc.jl index ee0ef24e..4b136fe5 100644 --- a/test/misc.jl +++ b/test/misc.jl @@ -1,50 +1,10 @@ -@testset "Rate type" begin - - default = Yields.Rate{Float64,typeof(Yields.DEFAULT_COMPOUNDING)} - - y = Yields.Constant(0.05) - @test Yields.__ratetype(y) == Yields.Rate{Float64, Periodic} - @test Yields.__ratetype(typeof(Periodic(0.05,2))) == Yields.Rate{Float64, Periodic} - @test Yields.FinanceCore.CompoundingFrequency(y) == Periodic(1) - - y = Yields.Constant(Continuous(0.05)) - @test Yields.__ratetype(y) == Yields.Rate{Float64, Continuous} - @test Yields.__ratetype(typeof(Continuous(0.05))) == Yields.Rate{Float64, Continuous} - @test Yields.FinanceCore.CompoundingFrequency(y) == Continuous() - - y = Yields.Step([0.02,0.05], [1,2]) - @test Yields.__ratetype(y) == Yields.Rate{Float64, Periodic} - @test Yields.FinanceCore.CompoundingFrequency(y) == Periodic(1) - - y = Yields.Forward( [0.01,0.02,0.03] ) - @test Yields.__ratetype(y) == default - - rates =[0.01, 0.01, 0.03, 0.05, 0.07, 0.16, 0.35, 0.92, 1.40, 1.74, 2.31, 2.41] ./ 100 - mats = [1/12, 2/12, 3/12, 6/12, 1, 2, 3, 5, 7, 10, 20, 30] - - y = Yields.CMT(rates,mats) - @test Yields.__ratetype(y) == default - @test Yields.FinanceCore.CompoundingFrequency(y) == Continuous() - - - combination = y + y - @test Yields.__ratetype(combination) == default - - -end - -@testset "type coercion" begin - @test Yields.__coerce_rate(0.05, Periodic(1)) == Periodic(0.05,1) - @test Yields.__coerce_rate(Periodic(0.05,12), Periodic(1)) == Periodic(0.05,12) -end - #Issue #117 @testset "DecFP" begin import DecFP - @test Yields.Periodic(1/DecFP.Dec64(1/6)) == Yields.Periodic(6) - mats = convert.(DecFP.Dec64,[1/12, 2/12, 3/12, 6/12, 1, 2, 3, 5, 7, 10, 20, 30]) - rates = convert.(DecFP.Dec64,[0.01, 0.01, 0.03, 0.05, 0.07, 0.16, 0.35, 0.92, 1.40, 1.74, 2.31, 2.41] ./ 100) - y = Yields.CMT(rates,mats) - @test y isa Yields.AbstractYieldCurve + @test FinanceModels.Periodic(1 / DecFP.Dec64(1 / 6)) == FinanceModels.Periodic(6) + mats = convert.(DecFP.Dec64, [1 / 12, 2 / 12, 3 / 12, 6 / 12, 1, 2, 3, 5, 7, 10, 20, 30]) + rates = convert.(DecFP.Dec64, [0.01, 0.01, 0.03, 0.05, 0.07, 0.16, 0.35, 0.92, 1.40, 1.74, 2.31, 2.41] ./ 100) + y = fit(Spline.Linear(), CMTYield.(rates, mats), Fit.Bootstrap()) + @test y isa FinanceModels.Yield.AbstractYieldModel end \ No newline at end of file diff --git a/test/runtests.jl b/test/runtests.jl index e7231ba2..09e7432e 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,9 +1,18 @@ -using Yields +using Test +using FinanceCore +using Accessors + +# older Test tests below +# eventually covert these into TestItemRunner + +using FinanceModels using Test include("generic.jl") -include("bootstrap.jl") -include("RateCombination.jl") +include("sp.jl") + +include("Yield.jl") +include("CompositeYield.jl") include("SmithWilson.jl") include("ActuaryUtilities.jl") diff --git a/test/sp.jl b/test/sp.jl new file mode 100644 index 00000000..e13bf65b --- /dev/null +++ b/test/sp.jl @@ -0,0 +1,59 @@ + +# Cashflows +@testset "Cashflows" begin + c = Cashflow(1.0, 1.0) + + p = Projection(c) + + @test collect(p) == [c] + @test collect(c) == [c] +end + +# fixed bonds +@testset "Fixed Bonds" begin + c = Bond.Fixed(0.05, Periodic(1), 3.0) + p = Projection(c) + + @test collect(p) == [Cashflow(0.05, 1.0), Cashflow(0.05, 2.0), Cashflow(1.05, 3.0)] + @test collect(c) == [Cashflow(0.05, 1.0), Cashflow(0.05, 2.0), Cashflow(1.05, 3.0)] + @test collect(Bond.Fixed(0.05, Periodic(1), 2.5)) == [Cashflow(0.05, 0.5), Cashflow(0.05, 1.5), Cashflow(1.05, 2.5)] + @test collect(Bond.Fixed(0.05, Periodic(1), 1)) == [Cashflow(1.05, 1.0)] + + @test pv(Yield.Constant(0.05), Bond.Fixed(0.05, Periodic(1), 3.0)) ≈ 1.0 +end + +@testset "Floating Bonds" begin + p = Projection( + Bond.Floating(0.02, Periodic(1), 3.0, "SOFR"), + Dict("SOFR" => Yield.Constant(0.05)), + CashflowProjection(), + ) + + @test all(collect(p) .≈ [Cashflow(0.07, 1.0), Cashflow(0.07, 2.0), Cashflow(1.07, 3.0)]) +end + +@testset "Composite Contracts" begin + a = Bond.Fixed(0.05, Periodic(1), 3.0) + b = Bond.Fixed(0.1, Periodic(4), 3.0) + c = FinanceCore.Composite(a, b) + + p = Projection(c) + @test collect(p) == [collect(a); collect(b)] +end + +@testset "Fit Models" begin + + @testset "Yield Models" begin + qs = [ + Quote(1.0, Bond.Fixed(0.05, Periodic(1), 3.0)), + Quote(1.0, Bond.Fixed(0.07, Periodic(1), 3.0)), + ] + @test fit(Yield.Constant(), qs, Fit.Loss(x -> abs2(x))).rate ≈ Yield.Constant(0.0602).rate atol = 1e-4 + + end + +end +@testset "Yield Models" begin + y = Yield.Constant(0.05) + @test Yield.discount(y, 5) ≈ 1 / (1.05)^5 +end \ No newline at end of file