RiskPremium.jl (5664B)
1 module RiskPremium 2 3 using CSV, DataFrames, Dates, Statistics, LinearAlgebra, SpecialFunctions 4 using Plots 5 pgfplotsx() 6 7 export run_regression, format_table, make_plot 8 9 """ 10 newey_west_vcov(X, residuals; lag=12) 11 12 Newey-West HAC variance-covariance matrix with Bartlett kernel. 13 Matches R's sandwich::NeweyWest(model, lag=12, prewhite=FALSE). 14 """ 15 function newey_west_vcov(X::Matrix{Float64}, e::Vector{Float64}; lag::Int=12) 16 n, k = size(X) 17 XX_inv = inv(X'X) 18 19 # Meat: S = Σ w(l) * [Γ(l) + Γ(l)'] with Bartlett weights 20 S = zeros(k, k) 21 for l in 0:lag 22 w = 1.0 - l / (lag + 1) # Bartlett kernel 23 Γ = zeros(k, k) 24 for t in (l+1):n 25 Γ .+= e[t] * e[t-l] .* (X[t,:] * X[t-l,:]') 26 end 27 if l == 0 28 S .+= w .* Γ 29 else 30 S .+= w .* (Γ .+ Γ') 31 end 32 end 33 34 V = XX_inv * S * XX_inv 35 return V 36 end 37 38 """ 39 run_regression(predict; sample_filter=nothing) -> NamedTuple 40 41 Run OLS: rmrf_y3 ~ dp + cay + rf with Newey-West(12) standard errors. 42 Returns (coefs, se, r2, nobs, residuals, fitted). 43 """ 44 function run_regression(predict::DataFrame; sample_filter=nothing) 45 df = isnothing(sample_filter) ? copy(predict) : filter(sample_filter, predict) 46 47 y = Float64.(df.rmrf_y3) 48 n = length(y) 49 X = hcat(ones(n), Float64.(df.dp), Float64.(df.cay), Float64.(df.rf)) 50 varnames = [:constant, :dp, :cay, :rf] 51 52 β = X \ y 53 ŷ = X * β 54 e = y .- ŷ 55 56 ss_res = sum(e.^2) 57 ss_tot = sum((y .- mean(y)).^2) 58 r2 = 1.0 - ss_res / ss_tot 59 60 V = newey_west_vcov(X, e; lag=12) 61 se = sqrt.(diag(V)) 62 63 coefs = Dict(varnames .=> β) 64 ses = Dict(varnames .=> se) 65 66 return (; coefs, se=ses, r2, nobs=n, residuals=e, fitted=ŷ, β, varnames) 67 end 68 69 """ 70 format_table(result) -> String 71 72 Format regression output as text table (replaces stargazer). 73 """ 74 function format_table(result) 75 lines = String[] 76 push!(lines, "~~~R") 77 push!(lines, "===========================================================") 78 push!(lines, " Future Excess Returns ") 79 push!(lines, "-----------------------------------------------------------") 80 81 labels = Dict(:dp => "D/P ratio", :cay => "cay", :rf => "T-bill (three-month)", :constant => "Constant") 82 83 for var in [:dp, :cay, :rf, :constant] 84 b = result.coefs[var] 85 s = result.se[var] 86 z = abs(b / s) 87 p = 2.0 * (1.0 - _Φ(z)) 88 st = p < 0.01 ? "***" : p < 0.05 ? "**" : p < 0.10 ? "*" : "" 89 push!(lines, "$(rpad(labels[var], 35))$(lpad(string(round(b, digits=3)) * st, 20))") 90 push!(lines, "$(rpad("", 35))$(lpad("(" * string(round(s, digits=3)) * ")", 20))") 91 push!(lines, "") 92 end 93 94 push!(lines, "$(rpad("Observations", 35))$(lpad(string(result.nobs), 20))") 95 push!(lines, "$(rpad("R2", 35))$(lpad(string(round(result.r2, digits=3)), 20))") 96 push!(lines, "-----------------------------------------------------------") 97 push!(lines, "Notes: ***Significant at the 1 percent level.") 98 push!(lines, " **Significant at the 5 percent level. ") 99 push!(lines, " *Significant at the 10 percent level. ") 100 push!(lines, "~~~") 101 return join(lines, "\n") 102 end 103 104 # Standard normal CDF 105 function _Φ(x) 106 return 0.5 * erfc(-x / sqrt(2.0)) 107 end 108 109 """ 110 make_plot(predict) 111 112 Plot expected vs realized excess returns. Uses Plots.jl with PGFPlotsX backend. 113 """ 114 function make_plot(predict::DataFrame) 115 dates = Date.( 116 div.(predict.dateym, 100), 117 mod.(predict.dateym, 100), 118 1 119 ) 120 121 # Major ticks every 10 years, minor every 5 122 yr_min = year(dates[1]) 123 yr_max = year(dates[end]) 124 major_years = (yr_min ÷ 10 * 10):10:(yr_max ÷ 10 * 10 + 10) 125 minor_years = (yr_min ÷ 5 * 5):5:(yr_max ÷ 5 * 5 + 5) 126 xtick_major = Date.(major_years, 1, 1) 127 xtick_minor = Date.(minor_years, 1, 1) 128 129 p = plot(dates, 100 .* predict.rmrf_y3; 130 label = "Realized", 131 color = :steelblue, 132 alpha = 0.75, 133 lw = 0.5, 134 marker = (:circle, 2, 0.5, :steelblue), 135 xlabel = "", 136 ylabel = "Returns (percent)", 137 legend = :bottomleft, 138 framestyle = :box, 139 xticks = (xtick_major, [string(year(d)) * "-M" * string(month(d)) for d in xtick_major]), 140 minorticks = 2, 141 ) 142 plot!(p, dates, 100 .* predict.exp_rmrf; 143 label = "Expected", 144 color = :indianred, 145 alpha = 0.75, 146 lw = 0.5, 147 marker = (:circle, 2, 0.5, :indianred), 148 ) 149 150 savefig(p, "output/predict.pdf") 151 savefig(p, "output/predict.png") 152 println("Wrote output/predict.pdf and output/predict.png") 153 end 154 155 # When run as script 156 if abspath(PROGRAM_FILE) == @__FILE__ 157 predict = CSV.read("tmp/predict.csv", DataFrame) 158 predict.year = div.(predict.dateym, 100) 159 160 r1 = run_regression(predict; sample_filter = r -> r.year < 2011) 161 println("In-sample (year < 2011):") 162 println(" β_dp=$(round(r1.coefs[:dp], digits=3)), β_cay=$(round(r1.coefs[:cay], digits=3)), β_rf=$(round(r1.coefs[:rf], digits=3))") 163 164 r2 = run_regression(predict) 165 println("\nFull sample:") 166 println(" β_dp=$(round(r2.coefs[:dp], digits=3)), β_cay=$(round(r2.coefs[:cay], digits=3)), β_rf=$(round(r2.coefs[:rf], digits=3))") 167 168 predict.exp_rmrf = r2.fitted 169 CSV.write("output/predict.csv", select(predict, :dateym, :dp, :rf, :rmrf_y3, :cay, :exp_rmrf)) 170 171 table = format_table(r2) 172 mkpath("tmp") 173 write("tmp/reg_update.txt", table) 174 println("\nWrote output/predict.csv and tmp/reg_update.txt") 175 176 make_plot(predict) 177 end 178 179 end # module