GSW.jl (48351B)
1 # -------------------------------------------------------------------------------------------------- 2 # ImportYields.jl 3 4 # Collection of functions that import Treasury Yields data 5 # -------------------------------------------------------------------------------------------------- 6 7 8 # -------------------------------------------------------------------------------------------------- 9 # GSW Parameter Type Definition 10 # -------------------------------------------------------------------------------------------------- 11 12 """ 13 GSWParameters 14 15 Structure to hold Gürkaynak-Sack-Wright Nelson-Siegel-Svensson model parameters. 16 17 # Fields 18 - `β₀::Float64`: Level parameter (BETA0) 19 - `β₁::Float64`: Slope parameter (BETA1) 20 - `β₂::Float64`: Curvature parameter (BETA2) 21 - `β₃::Float64`: Second curvature parameter (BETA3) - may be missing if model uses 3-factor version 22 - `τ₁::Float64`: First decay parameter (TAU1, must be positive) 23 - `τ₂::Float64`: Second decay parameter (TAU2, must be positive) - may be missing if model uses 3-factor version 24 25 # Examples 26 ```julia 27 # Create GSW parameters manually (4-factor model) 28 params = GSWParameters(5.0, -2.0, 1.5, 0.8, 2.5, 0.5) 29 30 # Create GSW parameters for 3-factor model (when τ₂/β₃ are missing) 31 params_3factor = GSWParameters(5.0, -2.0, 1.5, missing, 2.5, missing) 32 33 # Create from DataFrame row 34 df = import_gsw_parameters() 35 params = GSWParameters(df[1, :]) # First row 36 37 # Access individual parameters 38 println("Level: ", params.β₀) 39 println("Slope: ", params.β₁) 40 ``` 41 42 # Notes 43 - Constructor validates that available decay parameters are positive 44 - Handles missing values for τ₂ and β₃ (common when using 3-factor Nelson-Siegel model) 45 - When τ₂ or β₃ are missing, the model degenerates to the 3-factor Nelson-Siegel form 46 - Can be constructed from DataFrameRow for convenience 47 """ 48 struct GSWParameters 49 β₀::Float64 # Level 50 β₁::Float64 # Slope 51 β₂::Float64 # Curvature 1 52 β₃::Union{Float64, Missing} # Curvature 2 (may be missing for 3-factor model) 53 τ₁::Float64 # Decay 1 (must be positive) 54 τ₂::Union{Float64, Missing} # Decay 2 (may be missing for 3-factor model) 55 56 # Inner constructor with validation 57 # Returns `missing` (not a GSWParameters) when core fields are missing 58 function GSWParameters(β₀, β₁, β₂, β₃, τ₁, τ₂) 59 60 # Check if core parameters are missing — return missing instead of constructing 61 if ismissing(β₀) || ismissing(β₁) || ismissing(β₂) || ismissing(τ₁) 62 return missing 63 end 64 65 # Validate that decay parameters are positive 66 if τ₁ <= 0 67 throw(ArgumentError("First decay parameter τ₁ must be positive, got τ₁=$τ₁")) 68 end 69 if !ismissing(τ₂) && τ₂ <= 0 70 throw(ArgumentError("Second decay parameter τ₂ must be positive when present, got τ₂=$τ₂")) 71 end 72 73 new( 74 Float64(β₀), Float64(β₁), Float64(β₂), 75 ismissing(β₃) ? missing : Float64(β₃), 76 Float64(τ₁), 77 ismissing(τ₂) ? missing : Float64(τ₂) 78 ) 79 end 80 end 81 82 # Convenience constructors 83 """ 84 GSWParameters(row::DataFrameRow) 85 86 Create GSWParameters from a DataFrame row containing BETA0, BETA1, BETA2, BETA3, TAU1, TAU2 columns. 87 Handles missing values (including -999 flags) gracefully. 88 """ 89 function GSWParameters(row::DataFrameRow) 90 return GSWParameters(row.BETA0, row.BETA1, row.BETA2, row.BETA3, row.TAU1, row.TAU2) 91 end 92 93 """ 94 GSWParameters(row::NamedTuple) 95 96 Create GSWParameters from a NamedTuple containing the required fields. 97 Handles missing values (including -999 flags) gracefully. 98 """ 99 function GSWParameters(row::NamedTuple) 100 return GSWParameters(row.BETA0, row.BETA1, row.BETA2, row.BETA3, row.TAU1, row.TAU2) 101 end 102 103 104 """ 105 is_three_factor_model(params::GSWParameters) 106 107 Check if GSW parameters represent a 3-factor Nelson-Siegel model (missing β₃ and τ₂). 108 109 # Returns 110 - `Bool`: true if this is a 3-factor model, false if 4-factor Svensson model 111 """ 112 function is_three_factor_model(params::GSWParameters) 113 return ismissing(params.β₃) || ismissing(params.τ₂) 114 end 115 116 # Helper function to extract parameters as tuple, handling missing values 117 """ 118 _extract_params(params::GSWParameters) 119 120 Extract parameters as tuple for use in calculation functions. 121 For 3-factor models, uses τ₁ for both decay parameters and sets β₃=0. 122 """ 123 function _extract_params(params::GSWParameters) 124 # Handle 3-factor vs 4-factor models 125 if is_three_factor_model(params) 126 # For 3-factor model: set β₃=0 and use τ₁ for both decay parameters 127 β₃ = 0.0 128 τ₂ = ismissing(params.τ₂) ? params.τ₁ : params.τ₂ 129 else 130 β₃ = params.β₃ 131 τ₂ = params.τ₂ 132 end 133 134 return (params.β₀, params.β₁, params.β₂, β₃, params.τ₁, τ₂) 135 end 136 # -------------------------------------------------------------------------------------------------- 137 138 139 140 # -------------------------------------------------------------------------------------------------- 141 """ 142 import_gsw_parameters(; date_range=nothing, validate=true) 143 144 Import Gürkaynak-Sack-Wright (GSW) yield curve parameters from the Federal Reserve. 145 146 Downloads the daily GSW yield curve parameter estimates from the Fed's website and returns 147 a cleaned DataFrame with the Nelson-Siegel-Svensson model parameters. 148 149 # Arguments 150 - `date_range::Union{Nothing, Tuple{Date, Date}}`: Optional date range for filtering data. 151 If `nothing`, returns all available data. Default: `nothing` 152 - `validate::Bool`: Whether to validate input parameters and data quality. Default: `true` 153 154 # Returns 155 - `DataFrame`: Contains columns `:date`, `:BETA0`, `:BETA1`, `:BETA2`, `:BETA3`, `:TAU1`, `:TAU2` 156 157 # Throws 158 - `ArgumentError`: If date range is invalid 159 - `HTTP.ExceptionRequest.StatusError`: If download fails 160 - `Exception`: If data parsing fails 161 162 # Examples 163 ```julia 164 # Import all available data 165 df = import_gsw_parameters() 166 167 # Import data for specific date range 168 df = import_gsw_parameters(date_range=(Date("2020-01-01"), Date("2023-12-31"))) 169 170 # Import without validation (faster, but less safe) 171 df = import_gsw_parameters(validate=false) 172 ``` 173 174 # Notes 175 - Data source: Federal Reserve Economic Data (FRED) 176 - The GSW model uses the Nelson-Siegel-Svensson functional form 177 - Missing values in the original data are converted to `missing` 178 - Data is automatically sorted by date 179 - Additional variables: 180 - Zero-coupon yield,Continuously Compounded,SVENYXX 181 - Par yield,Coupon-Equivalent,SVENPYXX 182 - Instantaneous forward rate,Continuously Compounded,SVENFXX 183 - One-year forward rate,Coupon-Equivalent,SVEN1FXX 184 185 """ 186 function import_gsw_parameters(; 187 date_range::Union{Nothing, Tuple{Date, Date}} = nothing, 188 additional_variables::Vector{Symbol}=Symbol[], 189 validate::Bool = true) 190 191 192 # Download data with error handling 193 @info "Downloading GSW Yield Curve Parameters from Federal Reserve" 194 195 try 196 url_gsw = "https://www.federalreserve.gov/data/yield-curve-tables/feds200628.csv" 197 temp_file = Downloads.download(url_gsw) 198 199 # Parse CSV with proper error handling 200 df_gsw = CSV.read(temp_file, DataFrame, 201 skipto=11, 202 header=10, 203 silencewarnings=true) 204 205 # Clean up temporary file 206 rm(temp_file, force=true) 207 208 # Clean and process the data 209 df_clean = _clean_gsw_data(df_gsw, date_range; additional_variables=additional_variables) 210 211 212 if validate 213 _validate_gsw_data(df_clean) 214 end 215 216 @info "Successfully imported $(nrow(df_clean)) rows of GSW parameters" 217 return df_clean 218 219 catch e 220 if e isa Downloads.RequestError 221 throw(ArgumentError("Failed to download GSW data from Federal Reserve. Check internet connection.")) 222 elseif e isa CSV.Error 223 throw(ArgumentError("Failed to parse GSW data. The file format may have changed.")) 224 else 225 rethrow(e) 226 end 227 end 228 end 229 230 231 232 """ 233 _clean_gsw_data(df_raw, date_range) 234 235 Clean and format the raw GSW data from the Federal Reserve. 236 """ 237 function _clean_gsw_data(df_raw::DataFrame, 238 date_range::Union{Nothing, Tuple{Date, Date}}; 239 additional_variables::Vector{Symbol}=Symbol[]) 240 241 242 # Make a copy to avoid modifying original 243 df = copy(df_raw) 244 # Standardize column names 245 rename!(df, "Date" => "date") 246 247 # Apply date filtering if specified 248 if !isnothing(date_range) 249 start_date, end_date = date_range 250 if start_date > end_date 251 @warn "starting date posterior to end date ... shuffling them around" 252 start_date, end_date = min(start_date, end_date), max(start_date, end_date) 253 end 254 filter!(row -> start_date <= row.date <= end_date, df) 255 end 256 257 # Select and order relevant columns 258 parameter_cols = vcat( 259 [:BETA0, :BETA1, :BETA2, :BETA3, :TAU1, :TAU2], 260 intersect(additional_variables, propertynames(df)) 261 ) |> unique 262 select!(df, :date, parameter_cols...) 263 264 # Convert parameter columns to Float64, handling missing values 265 for col in parameter_cols 266 transform!(df, col => ByRow(_safe_parse_float) => col) 267 end 268 269 # Sort by date for consistency 270 sort!(df, :date) 271 272 return df 273 end 274 275 """ 276 _safe_parse_float(value) 277 278 Safely parse a value to Float64, returning missing for unparseable values. 279 Handles common flag values for missing data in economic datasets. 280 """ 281 function _safe_parse_float(value) 282 if ismissing(value) || value == "" 283 return missing 284 end 285 286 # Handle string values 287 if value isa AbstractString 288 parsed = tryparse(Float64, strip(value)) 289 if isnothing(parsed) 290 return missing 291 end 292 value = parsed 293 end 294 295 # Handle numeric values and check for common missing data flags 296 try 297 numeric_value = Float64(value) 298 299 # Common missing data flags in economic/financial datasets 300 if numeric_value in (-999.99, -999.0, -9999.0, -99.99) 301 return missing 302 end 303 304 return numeric_value 305 catch 306 return missing 307 end 308 end 309 310 """ 311 _validate_gsw_data(df) 312 313 Validate the cleaned GSW data for basic quality checks. 314 """ 315 function _validate_gsw_data(df::DataFrame) 316 if nrow(df) == 0 317 throw(ArgumentError("No data found for the specified date range")) 318 end 319 320 # Check for required columns 321 required_cols = [:date, :BETA0, :BETA1, :BETA2, :BETA3, :TAU1, :TAU2] 322 missing_cols = setdiff(required_cols, propertynames(df)) 323 if !isempty(missing_cols) 324 throw(ArgumentError("Missing required columns: $(missing_cols)")) 325 end 326 327 # Check for reasonable parameter ranges (basic sanity check) 328 param_cols = [:BETA0, :BETA1, :BETA2, :BETA3, :TAU1, :TAU2] 329 for col in param_cols 330 col_data = skipmissing(df[!, col]) |> collect 331 if length(col_data) == 0 332 @warn "Column $col contains only missing values" 333 end 334 end 335 336 # Check date continuity (warn if there are large gaps) 337 if nrow(df) > 1 338 date_diffs = diff(df.date) 339 large_gaps = findall(x -> x > Day(7), date_diffs) 340 if !isempty(large_gaps) 341 @warn "Found $(length(large_gaps)) gaps larger than 7 days in the data" 342 end 343 end 344 end 345 # -------------------------------------------------------------------------------------------------- 346 347 348 349 # -------------------------------------------------------------------------------------------------- 350 # GSW Core Calculation Functions 351 352 # Method 1: Using GSWParameters struct (preferred for clean API) 353 """ 354 gsw_yield(maturity, params::GSWParameters) 355 356 Calculate yield from GSW Nelson-Siegel-Svensson parameters using parameter struct. 357 358 # Arguments 359 - `maturity::Real`: Time to maturity in years (must be positive) 360 - `params::GSWParameters`: GSW parameter struct 361 362 # Returns 363 - `Float64`: Yield in percent (e.g., 5.0 for 5%) 364 365 # Examples 366 ```julia 367 params = GSWParameters(5.0, -2.0, 1.5, 0.8, 2.5, 0.5) 368 yield = gsw_yield(10.0, params) 369 ``` 370 """ 371 function gsw_yield(maturity::Real, params::GSWParameters) 372 return gsw_yield(maturity, _extract_params(params)...) 373 end 374 375 # Method 2: Using individual parameters (for flexibility and backward compatibility) 376 """ 377 gsw_yield(maturity, β₀, β₁, β₂, β₃, τ₁, τ₂) 378 379 Calculate yield from Gürkaynak-Sack-Wright Nelson-Siegel-Svensson parameters. 380 381 Computes the yield for a given maturity using the Nelson-Siegel-Svensson functional form 382 with the GSW parameter estimates. Automatically handles 3-factor vs 4-factor models. 383 384 # Arguments 385 - `maturity::Real`: Time to maturity in years (must be positive) 386 - `β₀::Real`: Level parameter (BETA0) 387 - `β₁::Real`: Slope parameter (BETA1) 388 - `β₂::Real`: Curvature parameter (BETA2) 389 - `β₃::Real`: Second curvature parameter (BETA3) - set to 0 or missing for 3-factor model 390 - `τ₁::Real`: First decay parameter 391 - `τ₂::Real`: Second decay parameter - can equal τ₁ for 3-factor model 392 393 # Returns 394 - `Float64`: Yield in percent (e.g., 5.0 for 5%) 395 396 # Throws 397 - `ArgumentError`: If maturity is non-positive or τ parameters are non-positive 398 399 # Examples 400 ```julia 401 # Calculate 1-year yield (4-factor model) 402 yield = gsw_yield(1.0, 5.0, -2.0, 1.5, 0.8, 2.5, 0.5) 403 404 # Calculate 10-year yield (3-factor model, β₃=0) 405 yield = gsw_yield(10.0, 5.0, -2.0, 1.5, 0.0, 2.5, 2.5) 406 ``` 407 408 # Notes 409 - Based on the Nelson-Siegel-Svensson functional form 410 - When β₃=0 or τ₂=τ₁, degenerates to 3-factor Nelson-Siegel model 411 - Returns yield in percentage terms (not decimal) 412 - Function is vectorizable: use `gsw_yield.(maturities, β₀, β₁, β₂, β₃, τ₁, τ₂)` 413 """ 414 function gsw_yield(maturity::Real, 415 β₀::Real, β₁::Real, β₂::Real, β₃::Real, τ₁::Real, τ₂::Real) 416 417 # Input validation 418 if maturity <= 0 419 throw(ArgumentError("Maturity must be positive, got $maturity")) 420 end 421 422 # For 3-factor model compatibility: if β₃ is 0 or very small, skip the fourth term 423 use_four_factor = abs(β₃) > 1e-10 && τ₂ > 0 424 425 # Nelson-Siegel-Svensson formula 426 t = Float64(maturity) 427 428 # Calculate decay terms 429 exp_t_τ₁ = exp(-t/τ₁) 430 431 # yield terms 432 term1 = β₀ # Level 433 term2 = β₁ * (1.0 - exp_t_τ₁) / (t/τ₁) # Slope 434 term3 = β₂ * ((1.0 - exp_t_τ₁) / (t/τ₁) - exp_t_τ₁) # First curvature 435 436 # Fourth term only for 4-factor Svensson model 437 term4 = if use_four_factor 438 exp_t_τ₂ = exp(-t/τ₂) 439 β₃ * ((1.0 - exp_t_τ₂) / (t/τ₂) - exp_t_τ₂) # Second curvature 440 else 441 0.0 442 end 443 444 yield = term1 + term2 + term3 + term4 445 446 return Float64(yield) 447 end 448 449 # Method 1: Using GSWParameters struct 450 """ 451 gsw_price(maturity, params::GSWParameters; face_value=1.0) 452 453 Calculate zero-coupon bond price from GSW parameters using parameter struct. 454 455 # Arguments 456 - `maturity::Real`: Time to maturity in years (must be positive) 457 - `params::GSWParameters`: GSW parameter struct 458 - `face_value::Real`: Face value of the bond (default: 1.0) 459 460 # Returns 461 - `Float64`: Bond price 462 463 # Examples 464 ```julia 465 params = GSWParameters(5.0, -2.0, 1.5, 0.8, 2.5, 0.5) 466 price = gsw_price(10.0, params) 467 ``` 468 """ 469 function gsw_price(maturity::Real, params::GSWParameters; face_value::Real = 1.0) 470 return gsw_price(maturity, _extract_params(params)..., face_value=face_value) 471 end 472 473 # Method 2: Using individual parameters 474 """ 475 gsw_price(maturity, β₀, β₁, β₂, β₃, τ₁, τ₂; face_value=1.0) 476 477 Calculate zero-coupon bond price from GSW Nelson-Siegel-Svensson parameters. 478 479 Computes the price of a zero-coupon bond using the yield derived from GSW parameters. 480 481 # Arguments 482 - `maturity::Real`: Time to maturity in years (must be positive) 483 - `β₀::Real`: Level parameter (BETA0) 484 - `β₁::Real`: Slope parameter (BETA1) 485 - `β₂::Real`: Curvature parameter (BETA2) 486 - `β₃::Real`: Second curvature parameter (BETA3) 487 - `τ₁::Real`: First decay parameter 488 - `τ₂::Real`: Second decay parameter 489 - `face_value::Real`: Face value of the bond (default: 1.0) 490 491 # Returns 492 - `Float64`: Bond price 493 494 # Throws 495 - `ArgumentError`: If maturity is non-positive, τ parameters are non-positive, or face_value is non-positive 496 497 # Examples 498 ```julia 499 # Calculate price of 1-year zero-coupon bond 500 price = gsw_price(1.0, 5.0, -2.0, 1.5, 0.8, 2.5, 0.5) 501 502 # Calculate price with different face value 503 price = gsw_price(1.0, 5.0, -2.0, 1.5, 0.8, 2.5, 0.5, face_value=1000.0) 504 ``` 505 506 # Notes 507 - Uses continuous compounding: P = F * exp(-r * t) 508 - Yield is converted from percentage to decimal for calculation 509 - Function is vectorizable: use `gsw_price.(maturities, β₀, β₁, β₂, β₃, τ₁, τ₂)` 510 """ 511 function gsw_price(maturity::Real, β₀::Real, β₁::Real, β₂::Real, β₃::Real, τ₁::Real, τ₂::Real; 512 face_value::Real = 1.0) 513 514 # Input validation 515 if maturity <= 0 516 throw(ArgumentError("Maturity must be positive, got $maturity")) 517 end 518 if face_value <= 0 519 throw(ArgumentError("Face value must be positive, got $face_value")) 520 end 521 522 # Handle any missing values 523 if any(ismissing, [β₀, β₁, β₂, β₃, τ₁, τ₂, maturity, face_value]) 524 return missing 525 end 526 527 # Get yield in percentage terms 528 yield_percent = gsw_yield(maturity, β₀, β₁, β₂, β₃, τ₁, τ₂) 529 530 if ismissing(yield_percent) 531 return missing 532 end 533 534 # Convert to decimal and calculate price using continuous compounding 535 continuous_rate = log(1.0 + yield_percent / 100.0) 536 price = face_value * exp(-continuous_rate * maturity) 537 538 return Float64(price) 539 end 540 541 # Method 1: Using GSWParameters struct 542 """ 543 gsw_forward_rate(maturity₁, maturity₂, params::GSWParameters) 544 545 Calculate instantaneous forward rate between two maturities using GSW parameter struct. 546 547 # Arguments 548 - `maturity₁::Real`: Start maturity in years (must be positive and < maturity₂) 549 - `maturity₂::Real`: End maturity in years (must be positive and > maturity₁) 550 - `params::GSWParameters`: GSW parameter struct 551 552 # Returns 553 - `Float64`: Forward rate (decimal rate) 554 555 # Examples 556 ```julia 557 params = GSWParameters(5.0, -2.0, 1.5, 0.8, 2.5, 0.5) 558 fwd_rate = gsw_forward_rate(2.0, 3.0, params) 559 ``` 560 """ 561 function gsw_forward_rate(maturity₁::Real, maturity₂::Real, params::GSWParameters) 562 return gsw_forward_rate(maturity₁, maturity₂, _extract_params(params)...) 563 end 564 565 # Method 2: Using individual parameters 566 """ 567 gsw_forward_rate(maturity₁, maturity₂, β₀, β₁, β₂, β₃, τ₁, τ₂) 568 569 Calculate instantaneous forward rate between two maturities using GSW parameters. 570 571 # Arguments 572 - `maturity₁::Real`: Start maturity in years (must be positive and < maturity₂) 573 - `maturity₂::Real`: End maturity in years (must be positive and > maturity₁) 574 - `β₀, β₁, β₂, β₃, τ₁, τ₂`: GSW parameters 575 576 # Returns 577 - `Float64`: Forward rate (decimal rate) 578 579 # Examples 580 ```julia 581 # Calculate 1-year forward rate starting in 2 years 582 fwd_rate = gsw_forward_rate(2.0, 3.0, 5.0, -2.0, 1.5, 0.8, 2.5, 0.5) 583 ``` 584 """ 585 function gsw_forward_rate(maturity₁::Real, maturity₂::Real, 586 β₀::Real, β₁::Real, β₂::Real, β₃::Real, τ₁::Real, τ₂::Real) 587 588 if maturity₁ <= 0 || maturity₂ <= maturity₁ 589 throw(ArgumentError("Must have 0 < maturity₁ < maturity₂, got maturity₁=$maturity₁, maturity₂=$maturity₂")) 590 end 591 592 # Handle missing values 593 if any(ismissing, [β₀, β₁, β₂, β₃, τ₁, τ₂, maturity₁, maturity₂]) 594 return missing 595 end 596 597 # Get prices at both maturities 598 p₁ = gsw_price(maturity₁, β₀, β₁, β₂, β₃, τ₁, τ₂) 599 p₂ = gsw_price(maturity₂, β₀, β₁, β₂, β₃, τ₁, τ₂) 600 601 if ismissing(p₁) || ismissing(p₂) 602 return missing 603 end 604 605 # Calculate forward rate: f = -ln(P₂/P₁) / (T₂ - T₁) 606 forward_rate_decimal = -log(p₂ / p₁) / (maturity₂ - maturity₁) 607 608 # Convert to percentage 609 return Float64(forward_rate_decimal) 610 end 611 612 # ------------------------------------------------------------------------------------------ 613 # Vectorized convenience functions 614 # ------------------------------------------------------------------------------------------ 615 616 """ 617 gsw_yield_curve(maturities, params::GSWParameters) 618 619 Calculate yields for multiple maturities using GSW parameter struct. 620 621 # Arguments 622 - `maturities::AbstractVector{<:Real}`: Vector of maturities in years 623 - `params::GSWParameters`: GSW parameter struct 624 625 # Returns 626 - `Vector{Float64}`: Vector of yields in percent 627 628 # Examples 629 ```julia 630 params = GSWParameters(5.0, -2.0, 1.5, 0.8, 2.5, 0.5) 631 maturities = [0.25, 0.5, 1, 2, 5, 10, 30] 632 yields = gsw_yield_curve(maturities, params) 633 ``` 634 """ 635 function gsw_yield_curve(maturities::AbstractVector{<:Real}, params::GSWParameters) 636 return gsw_yield.(maturities, Ref(params)) 637 end 638 639 """ 640 gsw_yield_curve(maturities, β₀, β₁, β₂, β₃, τ₁, τ₂) 641 642 Calculate yields for multiple maturities using GSW parameters. 643 644 # Arguments 645 - `maturities::AbstractVector{<:Real}`: Vector of maturities in years 646 - `β₀, β₁, β₂, β₃, τ₁, τ₂`: GSW parameters 647 648 # Returns 649 - `Vector{Float64}`: Vector of yields in percent 650 651 # Examples 652 ```julia 653 maturities = [0.25, 0.5, 1, 2, 5, 10, 30] 654 yields = gsw_yield_curve(maturities, 5.0, -2.0, 1.5, 0.8, 2.5, 0.5) 655 ``` 656 """ 657 function gsw_yield_curve(maturities::AbstractVector{<:Real}, β₀::Real, β₁::Real, β₂::Real, β₃::Real, τ₁::Real, τ₂::Real) 658 return gsw_yield.(maturities, β₀, β₁, β₂, β₃, τ₁, τ₂) 659 end 660 661 """ 662 gsw_price_curve(maturities, params::GSWParameters; face_value=1.0) 663 664 Calculate zero-coupon bond prices for multiple maturities using GSW parameter struct. 665 666 # Arguments 667 - `maturities::AbstractVector{<:Real}`: Vector of maturities in years 668 - `params::GSWParameters`: GSW parameter struct 669 - `face_value::Real`: Face value of bonds (default: 1.0) 670 671 # Returns 672 - `Vector{Float64}`: Vector of bond prices 673 674 # Examples 675 ```julia 676 params = GSWParameters(5.0, -2.0, 1.5, 0.8, 2.5, 0.5) 677 maturities = [0.25, 0.5, 1, 2, 5, 10, 30] 678 prices = gsw_price_curve(maturities, params) 679 ``` 680 """ 681 function gsw_price_curve(maturities::AbstractVector{<:Real}, params::GSWParameters; face_value::Real = 1.0) 682 return gsw_price.(maturities, Ref(params), face_value=face_value) 683 end 684 685 """ 686 gsw_price_curve(maturities, β₀, β₁, β₂, β₃, τ₁, τ₂; face_value=1.0) 687 688 Calculate zero-coupon bond prices for multiple maturities using GSW parameters. 689 690 # Arguments 691 - `maturities::AbstractVector{<:Real}`: Vector of maturities in years 692 - `β₀, β₁, β₂, β₃, τ₁, τ₂`: GSW parameters 693 - `face_value::Real`: Face value of bonds (default: 1.0) 694 695 # Returns 696 - `Vector{Float64}`: Vector of bond prices 697 698 # Examples 699 ```julia 700 maturities = [0.25, 0.5, 1, 2, 5, 10, 30] 701 prices = gsw_price_curve(maturities, 5.0, -2.0, 1.5, 0.8, 2.5, 0.5) 702 ``` 703 """ 704 function gsw_price_curve(maturities::AbstractVector{<:Real}, β₀::Real, β₁::Real, β₂::Real, β₃::Real, τ₁::Real, τ₂::Real; 705 face_value::Real = 1.0) 706 return gsw_price.(maturities, β₀, β₁, β₂, β₃, τ₁, τ₂, face_value=face_value) 707 end 708 # -------------------------------------------------------------------------------------------------- 709 710 711 712 713 # -------------------------------------------------------------------------------------------------- 714 # Return calculation functions 715 # ------------------------------------------------------------------------------------------ 716 717 # Method 1: Using individual parameters 718 """ 719 gsw_return(maturity, β₀_t, β₁_t, β₂_t, β₃_t, τ₁_t, τ₂_t, 720 β₀_t₋₁, β₁_t₋₁, β₂_t₋₁, β₃_t₋₁, τ₁_t₋₁, τ₂_t₋₁; 721 frequency=:daily, return_type=:log) 722 723 Calculate bond return between two periods using GSW parameters. 724 725 Computes the return on a zero-coupon bond between two time periods by comparing 726 the price today (with aged maturity) to the price in the previous period. 727 728 # Arguments 729 - `maturity::Real`: Original maturity of the bond in years 730 - `β₀_t, β₁_t, β₂_t, β₃_t, τ₁_t, τ₂_t`: GSW parameters at time t 731 - `β₀_t₋₁, β₁_t₋₁, β₂_t₋₁, β₃_t₋₁, τ₁_t₋₁, τ₂_t₋₁`: GSW parameters at time t-1 732 - `frequency::Symbol`: Return frequency (:daily, :monthly, :annual) 733 - `return_type::Symbol`: :log for log returns, :arithmetic for simple returns 734 735 # Returns 736 - `Float64`: Bond return 737 738 # Examples 739 ```julia 740 # Daily log return on 10-year bond 741 ret = gsw_return(10.0, 5.0, -2.0, 1.5, 0.8, 2.5, 0.5, # today's params 742 4.9, -1.9, 1.4, 0.9, 2.4, 0.6) # yesterday's params 743 744 # Monthly arithmetic return 745 ret = gsw_return(5.0, 5.0, -2.0, 1.5, 0.8, 2.5, 0.5, 746 4.9, -1.9, 1.4, 0.9, 2.4, 0.6, 747 frequency=:monthly, return_type=:arithmetic) 748 ``` 749 """ 750 function gsw_return(maturity::Real, 751 β₀_t::Real, β₁_t::Real, β₂_t::Real, β₃_t::Real, τ₁_t::Real, τ₂_t::Real, 752 β₀_t₋₁::Real, β₁_t₋₁::Real, β₂_t₋₁::Real, β₃_t₋₁::Real, τ₁_t₋₁::Real, τ₂_t₋₁::Real; 753 frequency::Symbol = :daily, 754 return_type::Symbol = :log) 755 756 # Input validation 757 if maturity <= 0 758 throw(ArgumentError("Maturity must be positive, got $maturity")) 759 end 760 761 valid_frequencies = [:daily, :monthly, :annual] 762 if frequency ∉ valid_frequencies 763 throw(ArgumentError("frequency must be one of $valid_frequencies, got $frequency")) 764 end 765 766 valid_return_types = [:log, :arithmetic] 767 if return_type ∉ valid_return_types 768 throw(ArgumentError("return_type must be one of $valid_return_types, got $return_type")) 769 end 770 771 # Handle missing values 772 all_params = [β₀_t, β₁_t, β₂_t, β₃_t, τ₁_t, τ₂_t, β₀_t₋₁, β₁_t₋₁, β₂_t₋₁, β₃_t₋₁, τ₁_t₋₁, τ₂_t₋₁] 773 if any(ismissing, all_params) 774 return missing 775 end 776 777 # Determine time step based on frequency 778 Δt = if frequency == :daily 779 1/360 # Using 360-day year convention 780 elseif frequency == :monthly 781 1/12 782 elseif frequency == :annual 783 1.0 784 end 785 786 # Calculate prices 787 # P_t: Price today of bond with remaining maturity (maturity - Δt) 788 aged_maturity = max(maturity - Δt, 0.001) # Avoid zero maturity 789 price_today = gsw_price(aged_maturity, β₀_t, β₁_t, β₂_t, β₃_t, τ₁_t, τ₂_t) 790 791 # P_t₋₁: Price yesterday of bond with original maturity 792 price_previous = gsw_price(maturity, β₀_t₋₁, β₁_t₋₁, β₂_t₋₁, β₃_t₋₁, τ₁_t₋₁, τ₂_t₋₁) 793 794 if ismissing(price_today) || ismissing(price_previous) 795 return missing 796 end 797 798 # Calculate return 799 if return_type == :log 800 return log(price_today / price_previous) 801 else # arithmetic 802 return (price_today - price_previous) / price_previous 803 end 804 end 805 806 807 # Method 2: Using GSWParameters structs 808 """ 809 gsw_return(maturity, params_t::GSWParameters, params_t₋₁::GSWParameters; frequency=:daily, return_type=:log) 810 811 Calculate bond return between two periods using GSW parameter structs. 812 813 # Arguments 814 - `maturity::Real`: Original maturity of the bond in years 815 - `params_t::GSWParameters`: GSW parameters at time t 816 - `params_t₋₁::GSWParameters`: GSW parameters at time t-1 817 - `frequency::Symbol`: Return frequency (:daily, :monthly, :annual) 818 - `return_type::Symbol`: :log for log returns, :arithmetic for simple returns 819 820 # Returns 821 - `Float64`: Bond return 822 823 # Examples 824 ```julia 825 params_today = GSWParameters(5.0, -2.0, 1.5, 0.8, 2.5, 0.5) 826 params_yesterday = GSWParameters(4.9, -1.9, 1.4, 0.9, 2.4, 0.6) 827 ret = gsw_return(10.0, params_today, params_yesterday) 828 ``` 829 """ 830 function gsw_return(maturity::Real, params_t::GSWParameters, params_t₋₁::GSWParameters; 831 frequency::Symbol = :daily, return_type::Symbol = :log) 832 return gsw_return(maturity, _extract_params(params_t)..., _extract_params(params_t₋₁)..., 833 frequency=frequency, return_type=return_type) 834 end 835 # -------------------------------------------------------------------------------------------------- 836 837 838 839 # Method 1: Using GSWParameters structs 840 """ 841 gsw_excess_return(maturity, params_t::GSWParameters, params_t₋₁::GSWParameters; 842 risk_free_maturity=0.25, frequency=:daily, return_type=:log) 843 844 Calculate excess return of a bond over the risk-free rate using GSW parameter structs. 845 846 # Arguments 847 - `maturity::Real`: Original maturity of the bond in years 848 - `params_t::GSWParameters`: GSW parameters at time t 849 - `params_t₋₁::GSWParameters`: GSW parameters at time t-1 850 - `risk_free_maturity::Real`: Maturity for risk-free rate calculation (default: 0.25 for 3-month) 851 - `frequency::Symbol`: Return frequency (:daily, :monthly, :annual) 852 - `return_type::Symbol`: :log for log returns, :arithmetic for simple returns 853 854 # Returns 855 - `Float64`: Excess return (bond return - risk-free return) 856 857 # Examples 858 ```julia 859 params_today = GSWParameters(5.0, -2.0, 1.5, 0.8, 2.5, 0.5) 860 params_yesterday = GSWParameters(4.9, -1.9, 1.4, 0.9, 2.4, 0.6) 861 excess_ret = gsw_excess_return(10.0, params_today, params_yesterday) 862 ``` 863 """ 864 function gsw_excess_return(maturity::Real, params_t::GSWParameters, params_t₋₁::GSWParameters; 865 risk_free_maturity::Real = 0.25, 866 frequency::Symbol = :daily, 867 return_type::Symbol = :log) 868 return gsw_excess_return(maturity, _extract_params(params_t)..., _extract_params(params_t₋₁)..., 869 risk_free_maturity=risk_free_maturity, frequency=frequency, return_type=return_type) 870 end 871 872 # Method 2: Using individual parameters 873 """ 874 gsw_excess_return(maturity, β₀_t, β₁_t, β₂_t, β₃_t, τ₁_t, τ₂_t, 875 β₀_t₋₁, β₁_t₋₁, β₂_t₋₁, β₃_t₋₁, τ₁_t₋₁, τ₂_t₋₁; 876 risk_free_maturity=0.25, frequency=:daily, return_type=:log) 877 878 Calculate excess return of a bond over the risk-free rate. 879 880 # Arguments 881 - Same as `gsw_return` plus: 882 - `risk_free_maturity::Real`: Maturity for risk-free rate calculation (default: 0.25 for 3-month) 883 884 # Returns 885 - `Float64`: Excess return (bond return - risk-free return) 886 """ 887 function gsw_excess_return(maturity::Real, 888 β₀_t::Real, β₁_t::Real, β₂_t::Real, β₃_t::Real, τ₁_t::Real, τ₂_t::Real, 889 β₀_t₋₁::Real, β₁_t₋₁::Real, β₂_t₋₁::Real, β₃_t₋₁::Real, τ₁_t₋₁::Real, τ₂_t₋₁::Real; 890 risk_free_maturity::Real = 0.25, 891 frequency::Symbol = :daily, 892 return_type::Symbol = :log) 893 894 # Calculate bond return 895 bond_return = gsw_return(maturity, β₀_t, β₁_t, β₂_t, β₃_t, τ₁_t, τ₂_t, 896 β₀_t₋₁, β₁_t₋₁, β₂_t₋₁, β₃_t₋₁, τ₁_t₋₁, τ₂_t₋₁, 897 frequency=frequency, return_type=return_type) 898 899 # Calculate risk-free return 900 rf_return = gsw_return(risk_free_maturity, β₀_t, β₁_t, β₂_t, β₃_t, τ₁_t, τ₂_t, 901 β₀_t₋₁, β₁_t₋₁, β₂_t₋₁, β₃_t₋₁, τ₁_t₋₁, τ₂_t₋₁, 902 frequency=frequency, return_type=return_type) 903 904 if ismissing(bond_return) || ismissing(rf_return) 905 return missing 906 end 907 908 return bond_return - rf_return 909 end 910 # -------------------------------------------------------------------------------------------------- 911 912 913 # -------------------------------------------------------------------------------------------------- 914 # -------------------------------------------------------------------------------------------------- 915 # GSW DataFrame Wrapper Functions 916 # ------------------------------------------------------------------------------------------ 917 """ 918 add_yields!(df, maturities; validate=true) 919 920 Add yield calculations to a DataFrame containing GSW parameters. 921 922 Adds columns with yields for specified maturities using the Nelson-Siegel-Svensson 923 model parameters in the DataFrame. 924 925 # Arguments 926 - `df::DataFrame`: DataFrame containing GSW parameters (must have columns: BETA0, BETA1, BETA2, BETA3, TAU1, TAU2) 927 - `maturities::Union{Real, AbstractVector{<:Real}}`: Maturity or vector of maturities in years 928 - `validate::Bool`: Whether to validate DataFrame structure (default: true) 929 930 # Returns 931 - `DataFrame`: Modified DataFrame with additional yield columns named `yield_Xy` (e.g., `yield_1y`, `yield_10y`) 932 933 # Examples 934 ```julia 935 df = import_gsw_parameters() 936 937 # Add single maturity 938 add_yields!(df, 10.0) 939 940 # Add multiple maturities 941 add_yields!(df, [1, 2, 5, 10, 30]) 942 943 # Add with custom maturity (fractional) 944 add_yields!(df, [0.25, 0.5, 1.0]) 945 ``` 946 947 # Notes 948 - Modifies the DataFrame in place 949 - Column names use format: `yield_Xy` where X is the maturity 950 - Handles missing parameter values gracefully 951 - Validates required columns are present 952 """ 953 function add_yields!(df::DataFrame, maturities::Union{Real, AbstractVector{<:Real}}; 954 validate::Bool = true) 955 956 if validate 957 _validate_gsw_dataframe(df) 958 end 959 960 # Ensure maturities is a vector 961 mat_vector = maturities isa Real ? [maturities] : collect(maturities) 962 963 # Validate maturities 964 if any(m -> m <= 0, mat_vector) 965 throw(ArgumentError("All maturities must be positive")) 966 end 967 968 # Add yield columns using GSWParameters struct 969 for maturity in mat_vector 970 col_name = _maturity_to_column_name("yield", maturity) 971 972 transform!(df, 973 AsTable([:BETA0, :BETA1, :BETA2, :BETA3, :TAU1, :TAU2]) => 974 ByRow(function(params) 975 gsw_params = GSWParameters(params) 976 if ismissing(gsw_params) 977 return missing 978 else 979 return gsw_yield(maturity, gsw_params) 980 end 981 end) => col_name) 982 end 983 984 return df 985 end 986 # -------------------------------------------------------------------------------------------------- 987 988 989 # -------------------------------------------------------------------------------------------------- 990 """ 991 add_prices!(df, maturities; face_value=100.0, validate=true) 992 993 Add zero-coupon bond price calculations to a DataFrame containing GSW parameters. 994 995 # Arguments 996 - `df::DataFrame`: DataFrame containing GSW parameters 997 - `maturities::Union{Real, AbstractVector{<:Real}}`: Maturity or vector of maturities in years 998 - `face_value::Real`: Face value of bonds (default: 100.0) 999 - `validate::Bool`: Whether to validate DataFrame structure (default: true) 1000 1001 # Returns 1002 - `DataFrame`: Modified DataFrame with additional price columns named `price_Xy` 1003 1004 # Examples 1005 ```julia 1006 df = import_gsw_parameters() 1007 1008 # Add prices for multiple maturities 1009 add_prices!(df, [1, 5, 10]) 1010 1011 # Add prices with different face value 1012 add_prices!(df, 10.0, face_value=1000.0) 1013 ``` 1014 """ 1015 function add_prices!(df::DataFrame, maturities::Union{Real, AbstractVector{<:Real}}; 1016 face_value::Real = 100.0, validate::Bool = true) 1017 1018 if validate 1019 _validate_gsw_dataframe(df) 1020 end 1021 1022 if face_value <= 0 1023 throw(ArgumentError("Face value must be positive, got $face_value")) 1024 end 1025 1026 # Ensure maturities is a vector 1027 mat_vector = maturities isa Real ? [maturities] : collect(maturities) 1028 1029 # Validate maturities 1030 if any(m -> m <= 0, mat_vector) 1031 throw(ArgumentError("All maturities must be positive")) 1032 end 1033 1034 # Add price columns using GSWParameters struct 1035 for maturity in mat_vector 1036 col_name = _maturity_to_column_name("price", maturity) 1037 1038 transform!(df, 1039 AsTable([:BETA0, :BETA1, :BETA2, :BETA3, :TAU1, :TAU2]) => 1040 ByRow(function(params) 1041 gsw_params = GSWParameters(params) 1042 if ismissing(gsw_params) 1043 return missing 1044 else 1045 return gsw_price(maturity, gsw_params, face_value=face_value) 1046 end 1047 end) => col_name) 1048 end 1049 1050 return df 1051 end 1052 # -------------------------------------------------------------------------------------------------- 1053 1054 1055 # -------------------------------------------------------------------------------------------------- 1056 """ 1057 add_returns!(df, maturity; frequency=:daily, return_type=:log, validate=true) 1058 1059 Add bond return calculations to a DataFrame containing GSW parameters. 1060 1061 Calculates returns by comparing bond prices across time periods. Requires DataFrame 1062 to be sorted by date and contain consecutive time periods. 1063 1064 # Arguments 1065 - `df::DataFrame`: DataFrame containing GSW parameters and dates (must have :date column) 1066 - `maturity::Real`: Bond maturity in years 1067 - `frequency::Symbol`: Return frequency (:daily, :monthly, :annual) 1068 - `return_type::Symbol`: :log for log returns, :arithmetic for simple returns 1069 - `validate::Bool`: Whether to validate DataFrame structure (default: true) 1070 1071 # Returns 1072 - `DataFrame`: Modified DataFrame with return column named `ret_Xy_frequency` 1073 (e.g., `ret_10y_daily`, `ret_5y_monthly`) 1074 1075 # Examples 1076 ```julia 1077 df = import_gsw_parameters() 1078 1079 # Add daily log returns for 10-year bond 1080 add_returns!(df, 10.0) 1081 1082 # Add monthly arithmetic returns for 5-year bond 1083 add_returns!(df, 5.0, frequency=:monthly, return_type=:arithmetic) 1084 ``` 1085 1086 # Notes 1087 - Requires DataFrame to be sorted by date 1088 - First row will have missing return (no previous period) 1089 - Uses lag of parameters to calculate returns properly 1090 """ 1091 function add_returns!(df::DataFrame, maturity::Real; 1092 frequency::Symbol = :daily, 1093 return_type::Symbol = :log, 1094 validate::Bool = true) 1095 1096 if validate 1097 _validate_gsw_dataframe(df, check_date=true) 1098 end 1099 1100 if maturity <= 0 1101 throw(ArgumentError("Maturity must be positive, got $maturity")) 1102 end 1103 1104 valid_frequencies = [:daily, :monthly, :annual] 1105 if frequency ∉ valid_frequencies 1106 throw(ArgumentError("frequency must be one of $valid_frequencies, got $frequency")) 1107 end 1108 1109 valid_return_types = [:log, :arithmetic] 1110 if return_type ∉ valid_return_types 1111 throw(ArgumentError("return_type must be one of $valid_return_types, got $return_type")) 1112 end 1113 1114 # Sort by date to ensure proper time series order 1115 sort!(df, :date) 1116 1117 # Determine time step based on frequency 1118 time_step = if frequency == :daily 1119 Day(1) 1120 elseif frequency == :monthly 1121 Day(30) # Approximate 1122 elseif frequency == :annual 1123 Day(360) # Using 360-day year 1124 end 1125 1126 # Create lagged parameter columns using PanelShift.jl 1127 param_cols = [:BETA0, :BETA1, :BETA2, :BETA3, :TAU1, :TAU2] 1128 for col in param_cols 1129 lag_col = Symbol("lag_$col") 1130 transform!(df, [:date, col] => 1131 ((dates, values) -> tlag(values, dates; n=time_step)) => 1132 lag_col) 1133 end 1134 1135 # Calculate returns using current and lagged parameters 1136 col_name = Symbol(string(_maturity_to_column_name("ret", maturity)) * "_" * string(frequency)) 1137 1138 transform!(df, 1139 AsTable(vcat(param_cols, [Symbol("lag_$col") for col in param_cols])) => 1140 ByRow(params -> begin 1141 current_params = GSWParameters(params.BETA0, params.BETA1, params.BETA2, 1142 params.BETA3, params.TAU1, params.TAU2) 1143 lagged_params = GSWParameters(params.lag_BETA0, params.lag_BETA1, params.lag_BETA2, 1144 params.lag_BETA3, params.lag_TAU1, params.lag_TAU2) 1145 if ismissing(current_params) || ismissing(lagged_params) 1146 missing 1147 else 1148 gsw_return(maturity, current_params, lagged_params, 1149 frequency=frequency, return_type=return_type) 1150 end 1151 end 1152 ) => col_name) 1153 1154 # Clean up temporary lagged columns 1155 select!(df, Not([Symbol("lag_$col") for col in param_cols])) 1156 1157 # Reorder columns to put return column first (after date) 1158 if :date in names(df) 1159 other_cols = filter(col -> col ∉ [:date, col_name], names(df)) 1160 select!(df, :date, col_name, other_cols...) 1161 end 1162 1163 return df 1164 end 1165 # -------------------------------------------------------------------------------------------------- 1166 1167 1168 # -------------------------------------------------------------------------------------------------- 1169 """ 1170 add_excess_returns!(df, maturity; risk_free_maturity=0.25, frequency=:daily, return_type=:log, validate=true) 1171 1172 Add excess return calculations (bond return - risk-free return) to DataFrame. 1173 1174 # Arguments 1175 - Same as `add_returns!` plus: 1176 - `risk_free_maturity::Real`: Maturity for risk-free rate (default: 0.25 for 3-month) 1177 1178 # Returns 1179 - `DataFrame`: Modified DataFrame with excess return column named `excess_ret_Xy_frequency` 1180 """ 1181 function add_excess_returns!(df::DataFrame, maturity::Real; 1182 risk_free_maturity::Real = 0.25, 1183 frequency::Symbol = :daily, 1184 return_type::Symbol = :log, 1185 validate::Bool = true) 1186 1187 if validate 1188 _validate_gsw_dataframe(df, check_date=true) 1189 end 1190 1191 if maturity <= 0 1192 throw(ArgumentError("Maturity must be positive, got $maturity")) 1193 end 1194 1195 valid_frequencies = [:daily, :monthly, :annual] 1196 if frequency ∉ valid_frequencies 1197 throw(ArgumentError("frequency must be one of $valid_frequencies, got $frequency")) 1198 end 1199 1200 valid_return_types = [:log, :arithmetic] 1201 if return_type ∉ valid_return_types 1202 throw(ArgumentError("return_type must be one of $valid_return_types, got $return_type")) 1203 end 1204 1205 # Sort by date to ensure proper time series order 1206 sort!(df, :date) 1207 1208 # Determine time step based on frequency 1209 time_step = if frequency == :daily 1210 Day(1) 1211 elseif frequency == :monthly 1212 Day(30) 1213 elseif frequency == :annual 1214 Day(360) 1215 end 1216 1217 # Create lagged parameter columns once (shared for both bond and rf returns) 1218 param_cols = [:BETA0, :BETA1, :BETA2, :BETA3, :TAU1, :TAU2] 1219 for col in param_cols 1220 lag_col = Symbol("lag_$col") 1221 transform!(df, [:date, col] => 1222 ((dates, values) -> tlag(values, dates; n=time_step)) => 1223 lag_col) 1224 end 1225 1226 # Calculate excess return directly in a single pass 1227 excess_col = Symbol(string(_maturity_to_column_name("excess_ret", maturity)) * "_" * string(frequency)) 1228 1229 transform!(df, 1230 AsTable(vcat(param_cols, [Symbol("lag_$col") for col in param_cols])) => 1231 ByRow(params -> begin 1232 current = GSWParameters(params.BETA0, params.BETA1, params.BETA2, 1233 params.BETA3, params.TAU1, params.TAU2) 1234 lagged = GSWParameters(params.lag_BETA0, params.lag_BETA1, params.lag_BETA2, 1235 params.lag_BETA3, params.lag_TAU1, params.lag_TAU2) 1236 if ismissing(current) || ismissing(lagged) 1237 missing 1238 else 1239 gsw_excess_return(maturity, current, lagged; 1240 risk_free_maturity=risk_free_maturity, 1241 frequency=frequency, return_type=return_type) 1242 end 1243 end) => excess_col) 1244 1245 # Clean up temporary lagged columns 1246 select!(df, Not([Symbol("lag_$col") for col in param_cols])) 1247 1248 return df 1249 end 1250 # -------------------------------------------------------------------------------------------------- 1251 1252 1253 1254 # -------------------------------------------------------------------------------------------------- 1255 # Convenience functions 1256 # -------------------------------------------------------------------------------------------------- 1257 """ 1258 gsw_curve_snapshot(params::GSWParameters; maturities=[0.25, 0.5, 1, 2, 5, 10, 30]) 1259 1260 Create a snapshot DataFrame of yields and prices for GSW parameters using parameter struct. 1261 1262 # Arguments 1263 - `params::GSWParameters`: GSW parameter struct 1264 - `maturities::AbstractVector`: Vector of maturities to calculate (default: standard curve) 1265 1266 # Returns 1267 - `DataFrame`: Contains columns :maturity, :yield, :price 1268 1269 # Examples 1270 ```julia 1271 params = GSWParameters(5.0, -2.0, 1.5, 0.8, 2.5, 0.5) 1272 curve = gsw_curve_snapshot(params) 1273 1274 # Custom maturities 1275 curve = gsw_curve_snapshot(params, maturities=[0.5, 1, 3, 5, 7, 10, 20, 30]) 1276 ``` 1277 """ 1278 function gsw_curve_snapshot(params::GSWParameters; 1279 maturities::AbstractVector = [0.25, 0.5, 1, 2, 5, 10, 30]) 1280 1281 yields = gsw_yield_curve(maturities, params) 1282 prices = gsw_price_curve(maturities, params) 1283 1284 return DataFrame( 1285 maturity = maturities, 1286 yield = yields, 1287 price = prices 1288 ) 1289 end 1290 1291 """ 1292 gsw_curve_snapshot(β₀, β₁, β₂, β₃, τ₁, τ₂; maturities=[0.25, 0.5, 1, 2, 5, 10, 30]) 1293 1294 Create a snapshot DataFrame of yields and prices for a single date's GSW parameters. 1295 1296 # Arguments 1297 - `β₀, β₁, β₂, β₃, τ₁, τ₂`: GSW parameters for a single date 1298 - `maturities::AbstractVector`: Vector of maturities to calculate (default: standard curve) 1299 1300 # Returns 1301 - `DataFrame`: Contains columns :maturity, :yield, :price 1302 1303 # Examples 1304 ```julia 1305 # Create yield curve snapshot 1306 curve = gsw_curve_snapshot(5.0, -2.0, 1.5, 0.8, 2.5, 0.5) 1307 1308 # Custom maturities 1309 curve = gsw_curve_snapshot(5.0, -2.0, 1.5, 0.8, 2.5, 0.5, 1310 maturities=[0.5, 1, 3, 5, 7, 10, 20, 30]) 1311 ``` 1312 """ 1313 function gsw_curve_snapshot(β₀::Real, β₁::Real, β₂::Real, β₃::Real, τ₁::Real, τ₂::Real; 1314 maturities::AbstractVector = [0.25, 0.5, 1, 2, 5, 10, 30]) 1315 1316 yields = gsw_yield_curve(maturities, β₀, β₁, β₂, β₃, τ₁, τ₂) 1317 prices = gsw_price_curve(maturities, β₀, β₁, β₂, β₃, τ₁, τ₂) 1318 1319 return DataFrame( 1320 maturity = maturities, 1321 yield = yields, 1322 price = prices 1323 ) 1324 end 1325 1326 # ------------------------------------------------------------------------------------------ 1327 # Internal helper functions 1328 # ------------------------------------------------------------------------------------------ 1329 """ 1330 _validate_gsw_dataframe(df; check_date=false) 1331 1332 Validate that DataFrame has required GSW parameter columns. 1333 """ 1334 function _validate_gsw_dataframe(df::DataFrame; check_date::Bool = false) 1335 required_cols = [:BETA0, :BETA1, :BETA2, :BETA3, :TAU1, :TAU2] 1336 missing_cols = setdiff(required_cols, propertynames(df)) 1337 1338 if !isempty(missing_cols) 1339 throw(ArgumentError("DataFrame missing required GSW parameter columns: $missing_cols")) 1340 end 1341 1342 if check_date && :date ∉ propertynames(df) 1343 throw(ArgumentError("DataFrame must contain :date column for return calculations")) 1344 end 1345 1346 if nrow(df) == 0 1347 throw(ArgumentError("DataFrame is empty")) 1348 end 1349 end 1350 1351 """ 1352 _maturity_to_column_name(prefix, maturity) 1353 1354 Convert maturity to standardized column name. 1355 """ 1356 function _maturity_to_column_name(prefix::String, maturity::Real) 1357 # Handle fractional maturities nicely 1358 if maturity == floor(maturity) 1359 return Symbol("$(prefix)_$(Int(maturity))y") 1360 else 1361 # For fractional, use decimal but clean up trailing zeros 1362 maturity_str = string(maturity) 1363 maturity_str = replace(maturity_str, r"\.?0+$" => "") # Remove trailing zeros 1364 return Symbol("$(prefix)_$(maturity_str)y") 1365 end 1366 end 1367 # -------------------------------------------------------------------------------------------------- 1368