FinanceRoutines.jl

Financial data routines for Julia
Log | Files | Refs | README | LICENSE

Yields.jl (15020B)


      1 @testset "GSW Treasury Yields" begin
      2 
      3     import Dates: Date, year
      4     import Statistics: mean, std
      5 
      6 
      7     # Test data import and basic structure
      8     @testset "Data Import and Basic Structure" begin
      9         # Test with original function name (backward compatibility)
     10         df_GSW = import_gsw_parameters(date_range = (Date("1970-01-01"), Date("1989-12-31")),
     11             additional_variables=[:SVENF05, :SVENF06, :SVENF07, :SVENF99])
     12         
     13         @test names(df_GSW) == ["date", "BETA0", "BETA1", "BETA2", "BETA3", "TAU1", "TAU2", "SVENF05", "SVENF06", "SVENF07"]
     14         @test nrow(df_GSW) > 0
     15         @test all(df_GSW.date .>= Date("1970-01-01"))
     16         @test all(df_GSW.date .<= Date("1989-12-31"))
     17         
     18         # Test date range validation
     19         @test_logs (:warn, "starting date posterior to end date ... shuffling them around") match_mode=:any import_gsw_parameters(date_range = (Date("1990-01-01"), Date("1980-01-01")));
     20         
     21         # Test missing data handling (-999 flags)
     22         @test any(ismissing, df_GSW.TAU2)  # Should have some missing τ₂ values in this period
     23     end
     24 
     25     # Test GSWParameters struct
     26     @testset "GSWParameters Struct" begin
     27         
     28         # Test normal construction
     29         params = GSWParameters(5.0, -2.0, 1.5, 0.8, 2.5, 0.5)
     30         @test params.β₀ == 5.0
     31         @test params.β₁ == -2.0
     32         @test params.τ₁ == 2.5
     33         @test params.τ₂ == 0.5
     34         
     35         # Test 3-factor model (missing τ₂, β₃)
     36         params_3f = GSWParameters(5.0, -2.0, 1.5, missing, 2.5, missing)
     37         @test ismissing(params_3f.β₃)
     38         @test ismissing(params_3f.τ₂)
     39         @test FinanceRoutines.is_three_factor_model(params_3f)
     40         @test !FinanceRoutines.is_three_factor_model(params)
     41         
     42         # Test validation
     43         @test_throws ArgumentError GSWParameters(5.0, -2.0, 1.5, 0.8, -1.0, 0.5)  # negative τ₁
     44         @test_throws ArgumentError GSWParameters(5.0, -2.0, 1.5, 0.8, 2.5, -0.5)  # negative τ₂
     45         
     46         # Test DataFrame row construction
     47         df_GSW = import_gsw_parameters(date_range = (Date("1985-01-01"), Date("1985-01-31")))
     48         if nrow(df_GSW) > 0
     49             params_from_row = GSWParameters(df_GSW[20, :])
     50             @test params_from_row isa GSWParameters
     51         end
     52 
     53     end
     54 
     55     # Test core calculation functions
     56     @testset "Core Calculation Functions" begin
     57 
     58         params = GSWParameters(5.0, -2.0, 1.5, 0.8, 2.5, 0.5)
     59         params_3f = GSWParameters(5.0, -2.0, 1.5, missing, 2.5, missing)
     60         
     61         # Test yield calculations
     62         yield_4f = gsw_yield(10.0, params)
     63         yield_3f = gsw_yield(10.0, params_3f)
     64         yield_scalar = gsw_yield(10.0, 5.0, -2.0, 1.5, 0.8, 2.5, 0.5)
     65         
     66         @test yield_4f isa Float64
     67         @test yield_3f isa Float64
     68         @test yield_scalar ≈ yield_4f
     69         
     70         # Test price calculations
     71         price_4f = gsw_price(10.0, params)
     72         price_3f = gsw_price(10.0, params_3f)
     73         price_scalar = gsw_price(10.0, 5.0, -2.0, 1.5, 0.8, 2.5, 0.5)
     74         
     75         @test price_4f isa Float64
     76         @test price_3f isa Float64
     77         @test price_scalar ≈ price_4f
     78         @test price_4f < 1.0  # Price should be less than face value for positive yields
     79         
     80         # Test forward rates
     81         fwd_4f = gsw_forward_rate(2.0, 3.0, params)
     82         fwd_scalar = gsw_forward_rate(2.0, 3.0, 5.0, -2.0, 1.5, 0.8, 2.5, 0.5)
     83         @test fwd_4f ≈ fwd_scalar
     84         
     85         # Test vectorized functions
     86         maturities = [0.25, 0.5, 1, 2, 5, 10, 30]
     87         yields = gsw_yield_curve(maturities, params)
     88         prices = gsw_price_curve(maturities, params)
     89         
     90         @test length(yields) == length(maturities)
     91         @test length(prices) == length(maturities)
     92         @test all(y -> y isa Float64, yields)
     93         @test all(p -> p isa Float64, prices)
     94         
     95         # Test input validation
     96         @test_throws ArgumentError gsw_yield(-1.0, params)  # negative maturity
     97         @test_throws ArgumentError gsw_price(-1.0, params)  # negative maturity
     98         @test_throws ArgumentError gsw_forward_rate(3.0, 2.0, params)  # invalid maturity order
     99     end
    100 
    101     # Test return calculations
    102     @testset "Return Calculations" begin
    103 
    104         params_t = GSWParameters(5.0, -2.0, 1.5, 0.8, 2.5, 0.5)
    105         params_t_minus_1 = GSWParameters(4.9, -1.9, 1.4, 0.9, 2.4, 0.6)
    106         
    107         # Test return calculation with structs
    108         ret_struct = gsw_return(10.0, params_t, params_t_minus_1)
    109         ret_scalar = gsw_return(10.0, 5.0, -2.0, 1.5, 0.8, 2.5, 0.5,
    110                                      4.9, -1.9, 1.4, 0.9, 2.4, 0.6)
    111         
    112         @test ret_struct ≈ ret_scalar
    113         @test ret_struct isa Float64
    114         
    115         # Test different return types
    116         ret_log = gsw_return(10.0, params_t, params_t_minus_1, return_type=:log)
    117         ret_arith = gsw_return(10.0, params_t, params_t_minus_1, return_type=:arithmetic)
    118         
    119         @test ret_log ≠ ret_arith  # Should be different
    120         @test ret_log isa Float64
    121         @test ret_arith isa Float64
    122         
    123         # Test excess returns
    124         excess_ret = gsw_excess_return(10.0, params_t, params_t_minus_1)
    125         @test excess_ret isa Float64
    126 
    127     end
    128 
    129     # Test DataFrame wrapper functions (original API)
    130     @testset "DataFrame Wrappers - Original API Tests" begin
    131 
    132         df_GSW = import_gsw_parameters(date_range = (Date("1970-01-01"), Date("1989-12-31")))
    133         
    134         # Test original functions with new names
    135         FinanceRoutines.add_yields!(df_GSW, 1.0)
    136         FinanceRoutines.add_prices!(df_GSW, 1.0)
    137         FinanceRoutines.add_returns!(df_GSW, 2.0, frequency=:daily, return_type=:log)
    138 
    139         
    140         # Verify columns were created
    141         @test "yield_1y" in names(df_GSW)
    142         @test "price_1y" in names(df_GSW)
    143         @test "ret_2y_daily" in names(df_GSW)
    144         
    145         # Test the original statistical analysis
    146         transform!(df_GSW, :date => (x -> year.(x) .÷ 10 * 10) => :date_decade)
    147         df_stats = combine(
    148             groupby(df_GSW, :date_decade),
    149             :yield_1y => ( x -> mean(skipmissing(x)) ) => :mean_yield,
    150             :yield_1y => ( x -> sqrt(std(skipmissing(x))) ) => :vol_yield,
    151             :price_1y => ( x -> mean(skipmissing(x)) ) => :mean_price,
    152             :price_1y => ( x -> sqrt(std(skipmissing(x))) ) => :vol_price,
    153             :ret_2y_daily => ( x -> mean(skipmissing(x)) ) => :mean_ret_2y_daily,
    154             :ret_2y_daily => ( x -> sqrt(std(skipmissing(x))) ) => :vol_ret_2y_daily
    155         )
    156         
    157         # Original tests - should still pass
    158         @test df_stats[1, :mean_yield] < df_stats[2, :mean_yield]
    159         @test df_stats[1, :vol_yield] < df_stats[2, :vol_yield]
    160         @test df_stats[1, :mean_price] > df_stats[2, :mean_price]
    161         @test df_stats[1, :vol_price] < df_stats[2, :vol_price]
    162         @test df_stats[1, :mean_ret_2y_daily] < df_stats[2, :mean_ret_2y_daily]
    163         @test df_stats[1, :vol_ret_2y_daily] < df_stats[2, :vol_ret_2y_daily]
    164     end
    165 
    166     # Test enhanced DataFrame wrapper functions
    167     @testset "DataFrame Wrappers - Enhanced API" begin
    168 
    169         df_GSW = import_gsw_parameters(date_range = (Date("1980-01-01"), Date("1985-12-31")))
    170         
    171         # Test multiple maturities at once
    172         FinanceRoutines.add_yields!(df_GSW, [0.5, 1, 2, 5, 10])
    173         expected_yield_cols = ["yield_0.5y", "yield_1y", "yield_2y", "yield_5y", "yield_10y"]
    174         @test all(col -> col in names(df_GSW), expected_yield_cols)
    175         
    176         # Test multiple prices
    177         FinanceRoutines.add_prices!(df_GSW, [1, 5, 10], face_value=100.0)
    178         expected_price_cols = ["price_1y", "price_5y", "price_10y"]
    179         @test all(col -> col in names(df_GSW), expected_price_cols)
    180         
    181         # Test different frequencies
    182         FinanceRoutines.add_returns!(df_GSW, 5, frequency=:monthly, return_type=:arithmetic)
    183         @test "ret_5y_monthly" in names(df_GSW)
    184         
    185         # Test excess returns
    186         FinanceRoutines.add_excess_returns!(df_GSW, 10, risk_free_maturity=0.25)
    187         @test "excess_ret_10y_daily" in names(df_GSW)
    188         
    189         # Test that calculations work with missing data
    190         @test any(!ismissing, df_GSW.yield_1y)
    191         @test any(!ismissing, df_GSW.price_1y)
    192     end
    193 
    194     # Test convenience functions
    195     @testset "Convenience Functions" begin
    196 
    197         params = GSWParameters(5.0, -2.0, 1.5, 0.8, 2.5, 0.5)
    198         
    199         # Test curve snapshot with struct
    200         curve_struct = FinanceRoutines.gsw_curve_snapshot(params)
    201         @test names(curve_struct) == ["maturity", "yield", "price"]
    202         @test nrow(curve_struct) == 7  # default maturities
    203         
    204         # Test curve snapshot with scalars
    205         curve_scalar = FinanceRoutines.gsw_curve_snapshot(5.0, -2.0, 1.5, 0.8, 2.5, 0.5)
    206         @test curve_struct.yield ≈ curve_scalar.yield
    207         @test curve_struct.price ≈ curve_scalar.price
    208         
    209         # Test custom maturities
    210         custom_maturities = [1, 3, 5, 7, 10]
    211         curve_custom = FinanceRoutines.gsw_curve_snapshot(params, maturities=custom_maturities)
    212         @test nrow(curve_custom) == length(custom_maturities)
    213         @test curve_custom.maturity == custom_maturities
    214     end
    215 
    216     # Test edge cases and robustness
    217     @testset "Edge Cases and Robustness" begin
    218         # Test very short and very long maturities
    219         params = GSWParameters(5.0, -2.0, 1.5, 0.8, 2.5, 0.5)
    220         
    221         yield_short = gsw_yield(0.001, params)  # Very short maturity
    222         yield_long = gsw_yield(100.0, params)   # Very long maturity
    223         @test yield_short isa Float64
    224         @test yield_long isa Float64
    225         
    226         # Test with extreme parameter values
    227         params_extreme = GSWParameters(0.0, 0.0, 0.0, 0.0, 10.0, 20.0)
    228         yield_extreme = gsw_yield(1.0, params_extreme)
    229         @test yield_extreme ≈ 0.0  # Should be zero with all β parameters = 0
    230         
    231         # Test missing data handling in calculations
    232         df_with_missing = DataFrame(
    233             date = [Date("2020-01-01")],
    234             BETA0 = [5.0], BETA1 = [-2.0], BETA2 = [1.5],
    235             BETA3 = [missing], TAU1 = [2.5], TAU2 = [missing]
    236         )
    237         
    238         FinanceRoutines.add_yields!(df_with_missing, 10.0)
    239         @test !ismissing(df_with_missing.yield_10y[1])  # Should work with 3-factor model
    240     end
    241 
    242     # Test performance and consistency
    243     @testset "Performance and Consistency" begin
    244         
    245         params = GSWParameters(5.0, -2.0, 1.5, 0.8, 2.5, 0.5)
    246         
    247         # Test that struct and scalar APIs give identical results
    248         maturities = [0.25, 0.5, 1, 2, 5, 10, 20, 30]
    249         
    250         yields_struct = gsw_yield.(maturities, Ref(params))
    251         yields_scalar = gsw_yield.(maturities, 5.0, -2.0, 1.5, 0.8, 2.5, 0.5)
    252         
    253         @test yields_struct ≈ yields_scalar
    254         
    255         prices_struct = gsw_price.(maturities, Ref(params))
    256         prices_scalar = gsw_price.(maturities, 5.0, -2.0, 1.5, 0.8, 2.5, 0.5)
    257         
    258         @test prices_struct ≈ prices_scalar
    259         
    260         # Test yield curve monotonicity assumptions don't break
    261         @test all(diff(yields_struct) .< 5.0)  # No huge jumps in yield curve
    262     end
    263 
    264     # Test 3-factor vs 4-factor model compatibility
    265     @testset "3-Factor vs 4-Factor Model Compatibility" begin
    266         # Create both model types
    267         params_4f = GSWParameters(5.0, -2.0, 1.5, 0.8, 2.5, 0.5)
    268         params_3f = GSWParameters(5.0, -2.0, 1.5, missing, 2.5, missing)
    269         
    270         # Test that 3-factor model gives reasonable results
    271         yield_4f = gsw_yield(10.0, params_4f)
    272         yield_3f = gsw_yield(10.0, params_3f)
    273         
    274         @test abs(yield_4f - yield_3f) < 2.0  # Should be reasonably close
    275         
    276         # Test DataFrame with mixed model periods
    277         df_mixed = DataFrame(
    278             date = [Date("2020-01-01"), Date("2020-01-02")],
    279             BETA0 = [5.0, 5.1], BETA1 = [-2.0, -2.1], BETA2 = [1.5, 1.4],
    280             BETA3 = [0.8, missing], TAU1 = [2.5, 2.4], TAU2 = [0.5, missing]
    281         )
    282         
    283         FinanceRoutines.add_yields!(df_mixed, 10.0)
    284         @test !ismissing(df_mixed.yield_10y[1])  # 4-factor period
    285         @test !ismissing(df_mixed.yield_10y[2])  # 3-factor period
    286     end
    287 
    288     @testset "Estimation of Yields (Excel function)" begin
    289 
    290         # Test basic bond_yield calculation
    291         @test FinanceRoutines.bond_yield(950, 1000, 0.05, 3.5, 2) ≈ 0.0663 atol=1e-3
    292         # Test bond at par (price = face_value should yield ≈ coupon_rate)
    293         @test FinanceRoutines.bond_yield(1000, 1000, 0.06, 5.0, 2) ≈ 0.06 atol=1e-4
    294         # Test premium bond (price > face_value should yield < coupon_rate)
    295         ytm_premium = FinanceRoutines.bond_yield(1050, 1000, 0.05, 10.0, 2)
    296         @test ytm_premium < 0.05
    297 
    298         # Test Excel API with provided example
    299         settlement = Date(2008, 2, 15)
    300         maturity = Date(2016, 11, 15)
    301         ytm_excel = FinanceRoutines.bond_yield_excel(settlement, maturity, 0.0575, 95.04287, 100.0, 
    302                                      frequency=2, basis=0)
    303         @test ytm_excel ≈ 0.065 atol=5e-4 # Excel YIELD returns 0.065 (6.5%)
    304 
    305         # Test Excel API consistency with direct bond_yield
    306         years = 8.75  # approximate years between Feb 2008 to Nov 2016
    307         ytm_direct = FinanceRoutines.bond_yield(95.04287, 100.0, 0.0575, years, 2)
    308         @test ytm_excel ≈ ytm_direct atol=1e-2
    309 
    310         # Test quarterly frequency
    311         @test FinanceRoutines.bond_yield(980, 1000, 0.04, 2.0, 4) > 0.04  # discount bond
    312         # Test annual frequency
    313         @test FinanceRoutines.bond_yield(1020, 1000, 0.03, 5.0, 1) < 0.03  # premium bond
    314         # Test case where Brent initially failed due to non-bracketing intervals
    315         @test FinanceRoutines.bond_yield_excel(Date("2014-04-24"), Date("2015-12-01"), 0.04, 105.46, 100.0, frequency=2) ≈ 0.0057 atol=5e-4
    316         # Two tests with fractional years
    317         @test FinanceRoutines.bond_yield_excel(Date("2013-10-08"), Date("2020-09-01"), 0.05, 116.76, 100.0; frequency=2) ≈ 0.0235 atol=5e-4
    318         @test FinanceRoutines.bond_yield_excel(Date("2014-07-31"), Date("2032-05-15"), 0.05, 114.083, 100.0; frequency=2) ≈ 0.0389 atol=5e-4
    319     end
    320 
    321     @testset "Missing value flag handling" begin
    322         @test ismissing(FinanceRoutines._safe_parse_float(-999.99))
    323         @test ismissing(FinanceRoutines._safe_parse_float(-999.0))
    324         @test ismissing(FinanceRoutines._safe_parse_float(-9999.0))
    325         @test ismissing(FinanceRoutines._safe_parse_float(-99.99))
    326         @test !ismissing(FinanceRoutines._safe_parse_float(-5.0))  # legitimate negative
    327         @test FinanceRoutines._safe_parse_float(3.14) ≈ 3.14
    328         @test ismissing(FinanceRoutines._safe_parse_float(""))
    329         @test ismissing(FinanceRoutines._safe_parse_float(missing))
    330         @test FinanceRoutines._safe_parse_float("2.5") ≈ 2.5
    331         @test ismissing(FinanceRoutines._safe_parse_float("abc"))
    332     end
    333 
    334 end  # @testset "GSW Extended Test Suite"
    335 
    336 
    337 
    338 
    339 
    340 
    341 
    342 
    343 
    344 
    345 
    346 
    347 
    348 
    349