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