FinanceRoutines.jl

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

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 # --------------------------------------------------------------------------------------------------