PortfolioUtils.jl (2764B)
1 # -------------------------------------------------------------------------------------------------- 2 # PortfolioUtils.jl 3 4 # Portfolio-level return calculations 5 # -------------------------------------------------------------------------------------------------- 6 7 8 # -------------------------------------------------------------------------------------------------- 9 """ 10 calculate_portfolio_returns(df, ret_col, date_col; 11 weighting=:value, weight_col=nothing, groups=nothing) 12 13 Calculate portfolio returns from individual stock returns. 14 15 # Arguments 16 - `df::AbstractDataFrame`: Panel data with stock returns 17 - `ret_col::Symbol`: Column name for returns 18 - `date_col::Symbol`: Column name for dates 19 20 # Keywords 21 - `weighting::Symbol=:value`: `:equal` for equal-weighted, `:value` for value-weighted 22 - `weight_col::Union{Nothing,Symbol}=nothing`: Column for weights (required if `weighting=:value`) 23 - `groups::Union{Nothing,Symbol,Vector{Symbol}}=nothing`: Optional grouping columns (e.g., size quintile) 24 25 # Returns 26 - `DataFrame`: Portfolio returns by date (and group if specified), with column `:port_ret` 27 28 # Examples 29 ```julia 30 # Equal-weighted portfolio returns 31 df_ew = calculate_portfolio_returns(df, :ret, :datem; weighting=:equal) 32 33 # Value-weighted by market cap 34 df_vw = calculate_portfolio_returns(df, :ret, :datem; weighting=:value, weight_col=:mktcap) 35 36 # Value-weighted by group (e.g., size quintile) 37 df_grouped = calculate_portfolio_returns(df, :ret, :datem; 38 weighting=:value, weight_col=:mktcap, groups=:size_quintile) 39 ``` 40 """ 41 function calculate_portfolio_returns(df::AbstractDataFrame, ret_col::Symbol, date_col::Symbol; 42 weighting::Symbol=:value, 43 weight_col::Union{Nothing,Symbol}=nothing, 44 groups::Union{Nothing,Symbol,Vector{Symbol}}=nothing) 45 46 if weighting == :value && isnothing(weight_col) 47 throw(ArgumentError("weight_col required for value-weighted portfolios")) 48 end 49 if weighting ∉ (:equal, :value) 50 throw(ArgumentError("weighting must be :equal or :value, got :$weighting")) 51 end 52 53 group_cols = if isnothing(groups) 54 [date_col] 55 else 56 vcat([date_col], groups isa Symbol ? [groups] : groups) 57 end 58 59 grouped = groupby(df, group_cols) 60 61 if weighting == :equal 62 return combine(grouped, ret_col => (r -> mean(skipmissing(r))) => :port_ret) 63 else 64 return combine(grouped, 65 [ret_col, weight_col] => ((r, w) -> begin 66 valid = .!ismissing.(r) .& .!ismissing.(w) 67 any(valid) || return missing 68 rv, wv = r[valid], w[valid] 69 sum(rv .* wv) / sum(wv) 70 end) => :port_ret) 71 end 72 end 73 # --------------------------------------------------------------------------------------------------