RiskPremium

Measuring the market risk premium
Log | Files | Refs

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