andrewrosemberg / PortfolioOpt.jl

Portfolio optimization
MIT License
14 stars 2 forks source link

PortfolioOpt

Simple package with Portfolio Optimization (PO) formulations using JuMP.jl.

Stable Dev Coverage Code Style: Blue ColPrac: Contributor's Guide on Collaborative Practices for Community Packages

Installation

julia> ] add PortfolioOpt

PO Strategies

The core functionalities of this package are implementations of risk measures (type PortfolioRiskMeasure) of the random variable representing the next period portfolio return (R = w'r). These are used to define the objective's terms (type ObjectiveTerm) and risk constraints (type RiskConstraint) of a PO formulation (type PortfolioFormulation). As with realistic applications, the decision maker might only have limited information about the individual asset returns, so their uncertainty may be described in different ways:

Currently acceptable uncertainty types:

Currently implemented PortfolioRiskMeasures are:

The PortfolioRiskMeasures can be used to define both the RiskConstraints and the ObjectiveTerms in a PortfolioFormulation that can be parsed into details of a JuMP.Model using the portfolio_model! function.

In addition, ObjectiveTerms can also be ConeRegularizers defined by a cone set (e.g. norm-2) and a linear transformation (default Identity).

VolumeMarket

The portfolio_model! modifies an existing model JuMP.Model with decision variables already created and which, ideally, are already bounded by budget and bound constraints. In order to help users define market constraints, fees and clearing processess, this package also implements an interface with OptimalBids.jl (a framework for working with generic markets) through a simple market type called VolumeMarket.

VolumeMarket represents market models that only allow the strategic agent to bid at market price, thus their decision is restricted to the amount/volume traded of each of the available assets.

The current implementation allows the user to specify:

Once an instance of a VolumeMarket is defined, one can call market_model to create a JuMP.Model with the equivalent constraints, objective terms and variables created. Moreover, after the strategic objective terms and constraints are added on top of this model, it can be passed to the change_bids! together with the VolumeMarket object to modify the volume_bids::Vector{Real}. Alternatively, change_bids! can receive the already calculated bids (if chosen elsewhere) or even just the PortfolioFormulation, leaving the work of creating the JuMP.Model and adding all constraints and objective terms (market based or strategy based) to this function.

A market with already defined strategic bids, i.e. volume_bids, can be cleared using the function clear_market! that receives the VolumeMarket and the clearing_prices::Vector{Real}.

To help backtesting, a type VolumeMarketHistory was created to contain:

Instances of VolumeMarketHistory are the input of sequential_backtest_market: a function that provides a basic backtest using provided strategy and VolumeMarketHistory for a specified date_range (that needs to have the same eltype as timestamp).

TestUtils

As an extra, some testing utilities are available through the submodule called TestUtils:

Example Markowitz with Empirical Forecaster

Simple example of backtest with an available strategy.

\begin{aligned}
    \max_{w} \quad & r'w \\
    s.t. \quad & w ' \Sigma w \leq V_0 \\
    & w \in \mathcal{X} \\
\end{aligned}
using Clarabel
using Distributions
using PortfolioOpt
using PortfolioOpt.TestUtils

# Read Prices 
prices = get_test_data();
numD, numA = size(prices) # A: Assets    D: Days

# Calculating returns 
returns_series = percentchange(prices);

# Backtest Parameters 
DEFAULT_SOLVER = optimizer_with_attributes(
    Clarabel.Optimizer, "verbose" => false, "max_iter" => 900000
)

date_range = timestamp(returns_series)[100:end];

# Backtest
backtest_results = Dict()
backtest_results["EP_markowitz_limit_var"], _ = sequential_backtest_market(
    VolumeMarketHistory(returns_series), date_range,
) do market, past_returns, ext
    # Parameters
    max_variance = 0.03 / market_budget(market) # standard deviation limit
    k_back = 60 # forecaster lookback

    # Prep
    numD, numA = size(past_returns)
    returns = values(past_returns)

    # Empirical Forecast
    Σ, r̄ = mean_variance(returns[(end - k_back):end, :])
    d = MvNormal(r̄, Σ)

    # PO Formulation
    formulation = PortfolioFormulation(MAX_SENSE, # Maximization problem
        ObjectiveTerm(ExpectedReturn(d)), # Objective: Max Expected return of forecasted distribution
        RiskConstraint(Variance(d), LessThan(max_variance)), # Risk: limit PO variance
    )

    change_bids!(market, formulation, DEFAULT_SOLVER)
end

Let's add a regularizer and another risk constraint

Example Markowitz with Empirical Forecaster, Soyster Uncertainty Box and L1 regularizer:


backtest_results["EP_markowitz_with_soyster_l1"], _ = sequential_backtest_market(
    VolumeMarketHistory(returns_series), date_range,
) do market, past_returns, ext
    # Parameters
    max_std = 0.03 / market_budget(market)
    R = -0.06 / market_budget(market)
    l1_penalty = -0.0003
    k_back = 60

    # Prep
    numD, numA = size(past_returns)
    returns = values(past_returns)

    # Empirical Forecast
    Σ, r̄ = mean_variance(returns[(end - k_back):end, :])
    d = MvNormal(r̄, Σ)

    formulation = PortfolioFormulation(MAX_SENSE,
        [ # Objective Terms:
            ObjectiveTerm(ExpectedReturn(d)), # Max Expected return of forecasted distribution
            ObjectiveTerm(ConeRegularizer(MOI.NormOneCone(numA+1)), l1_penalty) # Regularize decisions through norm-1 regularizer with `l1_penalty` coeficient
        ],
        [ # Risk Constraints:
            RiskConstraint(SqrtVariance(d), LessThan(max_std)), # limit PO standard deviation
            RiskConstraint(ExpectedReturn(BudgetSet(d, maximum(abs.(returns); dims=1)'[:] .- r̄, numA * 1.0)), GreaterThan(R)), # Worst case return has to be greater than `R`
        ]
    )

    change_bids!(market, formulation, DEFAULT_SOLVER)
end

Plot Results

using Plots
using Plots.PlotMeasures

plt = plot(;title="Culmulative Wealth",
    xlabel="Time",
    ylabel="Wealth",
    legend=:outertopright,
    left_margin=10mm
);
for (strategy_name, recorders) in backtest_results
    plot!(plt, 
        axes(get_records(recorders[:wealth]), 1), get_records(recorders[:wealth]).data;
        label=strategy_name,
    )
end
plt