FinanceRoutines.jl

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

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