FinanceRoutines.jl

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

commit 78d20e7e18786ff8ca6fc3788fbea6541444a29d
parent e0e61bbe6cab72da80f76a7d78a23cb6bd51d217
Author: Erik Loualiche <[email protected]>
Date:   Sun, 22 Mar 2026 11:50:44 -0500

Add import_FF5 and import_FF_momentum

- FF5: 5-factor model (mktrf, smb, hml, rmw, cma, rf) at daily/monthly/annual
- Momentum: single factor (mom) at daily/monthly/annual
- Both use shared _import_ff_factors helper
- Simplified _parse_ff_annual for broader file format compatibility
- 12 new test assertions

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

Diffstat:
Msrc/FinanceRoutines.jl | 2+-
Msrc/ImportFamaFrench.jl | 103+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Atest/UnitTests/FF5.jl | 35+++++++++++++++++++++++++++++++++++
Mtest/runtests.jl | 1+
4 files changed, 128 insertions(+), 13 deletions(-)

diff --git a/src/FinanceRoutines.jl b/src/FinanceRoutines.jl @@ -50,7 +50,7 @@ export gsw_yield, gsw_price, gsw_forward_rate, gsw_yield_curve, gsw_price_curve, gsw_return, gsw_excess_return # Fama-French data -export import_FF3 +export import_FF3, import_FF5, import_FF_momentum # WRDS # -- CRSP diff --git a/src/ImportFamaFrench.jl b/src/ImportFamaFrench.jl @@ -137,22 +137,15 @@ function _parse_ff_annual(zip_file; types=nothing, end if found_annual - # Skip the header line that comes after "Annual Factors" - if occursin(r"Mkt-RF|SMB|HML|RF", line) - continue - end - - if occursin(r"^\s*$", line) || occursin(r"[A-Za-z]{3,}", line[1:min(10, length(line))]) - if !occursin(r"^\s*$", line) && !occursin(r"^\s*\d{4}", line) - break - end - continue - end - + # Data lines start with a 4-digit year if occursin(r"^\s*\d{4}", line) clean_line = replace(line, r"[\r]" => "") push!(lines, clean_line) + elseif !isempty(lines) && occursin(r"^\s*$", line) + # Empty line after we've started collecting data = end of section + break end + # Otherwise skip (headers, sub-headers, blank lines before data) end end @@ -211,3 +204,89 @@ function _parse_ff_monthly(zip_file; types=nothing, end # -------------------------------------------------------------------------------------------------- + + +# -------------------------------------------------------------------------------------------------- +""" + import_FF5(;frequency::Symbol=:monthly) -> DataFrame + +Import Fama-French 5-factor model data directly from Ken French's data library. + +Downloads and parses the Fama-French 5-factor research data (market risk premium, +size, value, profitability, and investment factors plus the risk-free rate). + +# Arguments +- `frequency::Symbol=:monthly`: Data frequency. Options: `:monthly`, `:annual`, `:daily` + +# Returns +- `DataFrame` with columns: + - **Monthly**: `datem`, `mktrf`, `smb`, `hml`, `rmw`, `cma`, `rf` + - **Annual**: `datey`, `mktrf`, `smb`, `hml`, `rmw`, `cma`, `rf` + - **Daily**: `date`, `mktrf`, `smb`, `hml`, `rmw`, `cma`, `rf` + +Where: +- `mktrf`: Market return minus risk-free rate +- `smb`: Small minus big (size) +- `hml`: High minus low (value) +- `rmw`: Robust minus weak (profitability) +- `cma`: Conservative minus aggressive (investment) +- `rf`: Risk-free rate + +# Examples +```julia +monthly_ff5 = import_FF5() +annual_ff5 = import_FF5(frequency=:annual) +daily_ff5 = import_FF5(frequency=:daily) +``` + +# Data Source +Kenneth R. French Data Library: https://mba.tuck.dartmouth.edu/pages/faculty/ken.french/data_library.html +""" +function import_FF5(;frequency::Symbol=:monthly) + url_mth_yr = "https://mba.tuck.dartmouth.edu/pages/faculty/ken.french/ftp/F-F_Research_Data_5_Factors_2x3_CSV.zip" + url_daily = "https://mba.tuck.dartmouth.edu/pages/faculty/ken.french/ftp/F-F_Research_Data_5_Factors_2x3_daily_CSV.zip" + col_types = [String7, Float64, Float64, Float64, Float64, Float64, Float64] + + return _import_ff_factors(frequency, url_mth_yr, url_daily, col_types, + col_names_monthly = [:datem, :mktrf, :smb, :hml, :rmw, :cma, :rf], + col_names_annual = [:datey, :mktrf, :smb, :hml, :rmw, :cma, :rf], + col_names_daily = [:date, :mktrf, :smb, :hml, :rmw, :cma, :rf]) +end +# -------------------------------------------------------------------------------------------------- + + +# -------------------------------------------------------------------------------------------------- +""" + import_FF_momentum(;frequency::Symbol=:monthly) -> DataFrame + +Import Fama-French momentum factor from Ken French's data library. + +# Arguments +- `frequency::Symbol=:monthly`: Data frequency. Options: `:monthly`, `:annual`, `:daily` + +# Returns +- `DataFrame` with columns: + - **Monthly**: `datem`, `mom` + - **Annual**: `datey`, `mom` + - **Daily**: `date`, `mom` + +# Examples +```julia +monthly_mom = import_FF_momentum() +daily_mom = import_FF_momentum(frequency=:daily) +``` + +# Data Source +Kenneth R. French Data Library: https://mba.tuck.dartmouth.edu/pages/faculty/ken.french/data_library.html +""" +function import_FF_momentum(;frequency::Symbol=:monthly) + url_mth_yr = "https://mba.tuck.dartmouth.edu/pages/faculty/ken.french/ftp/F-F_Momentum_Factor_CSV.zip" + url_daily = "https://mba.tuck.dartmouth.edu/pages/faculty/ken.french/ftp/F-F_Momentum_Factor_daily_CSV.zip" + col_types = [String7, Float64] + + return _import_ff_factors(frequency, url_mth_yr, url_daily, col_types, + col_names_monthly = [:datem, :mom], + col_names_annual = [:datey, :mom], + col_names_daily = [:date, :mom]) +end +# -------------------------------------------------------------------------------------------------- diff --git a/test/UnitTests/FF5.jl b/test/UnitTests/FF5.jl @@ -0,0 +1,35 @@ +@testset "Importing Fama-French 5 factors and Momentum" begin + + import Dates + + # FF5 monthly + df_FF5_monthly = import_FF5(frequency=:monthly) + @test names(df_FF5_monthly) == ["datem", "mktrf", "smb", "hml", "rmw", "cma", "rf"] + @test nrow(df_FF5_monthly) >= (Dates.year(Dates.today()) - 1963 - 1) * 12 + + # FF5 annual + df_FF5_annual = import_FF5(frequency=:annual) + @test names(df_FF5_annual) == ["datey", "mktrf", "smb", "hml", "rmw", "cma", "rf"] + @test nrow(df_FF5_annual) >= Dates.year(Dates.today()) - 1963 - 2 + + # FF5 daily + df_FF5_daily = import_FF5(frequency=:daily) + @test names(df_FF5_daily) == ["date", "mktrf", "smb", "hml", "rmw", "cma", "rf"] + @test nrow(df_FF5_daily) >= 15_000 + + # Momentum monthly + df_mom_monthly = import_FF_momentum(frequency=:monthly) + @test "mom" in names(df_mom_monthly) + @test nrow(df_mom_monthly) > 1000 + + # Momentum annual + df_mom_annual = import_FF_momentum(frequency=:annual) + @test "mom" in names(df_mom_annual) + @test nrow(df_mom_annual) > 90 + + # Momentum daily + df_mom_daily = import_FF_momentum(frequency=:daily) + @test "mom" in names(df_mom_daily) + @test nrow(df_mom_daily) > 24_000 + +end diff --git a/test/runtests.jl b/test/runtests.jl @@ -13,6 +13,7 @@ import DataPipes: @p # -------------------------------------------------------------------------------------------------- const testsuite = [ "KenFrench", + "FF5", "WRDS", "betas", "Yields",