commit 78d20e7e18786ff8ca6fc3788fbea6541444a29d
parent e0e61bbe6cab72da80f76a7d78a23cb6bd51d217
Author: Erik Loualiche <eloualic@umn.edu>
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) <noreply@anthropic.com>
Diffstat:
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",