BrandtSolver.jl
A high-performance Julia package for solving discrete-time dynamic portfolio choice problems using the simulation-based method originally proposed by [1].
In many realistic financial models (e.g., stochastic interest rates, predictability in asset returns, non-tradable income), analytical solutions to portfolio choice problems are hard to come by. BrandtSolver.jl overcomes this by combining already existing simulations of asset paths with cross-sectional regressions to dynamically approximate the optimal portfolio policy backwards through time.
More specifically, in this package we consider portfolio choice problems at timesteps $n = 1, 2, \ldots, M$, where $M + 1$ is some terminal timestep. This portfolio choice problem at timestep $n$ is defined by an investor who maximizes the expected utility of their wealth at the terminal timestep $M + 1$ by trading $N$ risky assets and a risk-free asset (cash). Formally the investor's problem at timestep $n$ is
\[ V_n(W_n, Z_n) = \max_{\{\omega_s\}_{m = n}^{M}} \mathbb{E}_n[u(W_{M + 1})]\]
subject to the sequence of budget constraints
\[ W_{m + 1} = W_m (\omega_m^\top R^e_{m + 1} + R_{m + 1})\]
for all $m \geq n$. Here $R^e_{m + 1}$ can be interpreted as the excess return of the risky assets over the risk-free asset, and $R_{m + 1}$ is the gross return of other processes that may depend on wealth $W_m$. Furthermore, $\{\omega_s\}_{m=n}^{M}$ is the sequence of portfolio weights chosen at times $m = n, \ldots, M$ and $u$ is the investor's utility function. The process $Z_n$ is a vector of state variables that are relevant for the investor's decision making. The goal of this package is to find $\{\omega_m\}_{m=1}^{M}$.
Extension: Wealth-Dependent Returns
A key feature of this implementation is that the gross return $R_{m+1}$ is not restricted to be exogenous. We allow $R_{m+1}$ to depend on the current level of wealth $W_m$ through the following structure:
\[ R_{m+1} = X_m + \frac{Y_m}{W_m}\]
where $X_m$ and $Y_m$ are functions of state variables contained in $Z_m$. This formulation is particularly powerful as it allows the algorithm to incorporate non-tradeable income or fixed costs. For instance, if $Y_m$ represents labor income, the budget constraint correctly captures that income is added to wealth regardless of the portfolio choice $\omega_m$.
Example
Consider an investor who receives a stochastic labor income $O_n$ at each timestep and saves a proportion $p \in [0,1]$ of that income. Let $R^f_n$ be the gross risk-free rate. The wealth at time $n+1$ is:
\[ W_{n+1} = W_n (\omega_n^\top R^e_{n+1} + R^f_n) + p O_n\]
By setting $X_n = R^f_n$ and $Y_n = p O_n$, this matches our budget constraint $W_{n+1} = W_n (\omega_n^\top R^e_{n+1} + R_{n+1})$.
Features
- Simulation-Based: Solves complex, multi-period portfolio optimization problems using generated market scenarios.
- State-Dependent Policies: Computes optimal portfolio weights that dynamically depend on exogenous state variables (e.g., interest rates) and the agent's current wealth.
- Arbitrary Utility Functions: Leverages Automatic Differentiation (
ForwardDiff.jl) to automatically compute exact higher-order Taylor expansions for any custom utility function.
Installation
You can install the package via the Julia REPL:
using Pkg
Pkg.add(url="https://github.com/sliemelela/BrandtSolver.jl")Quick start
using BrandtSolver
using FinancialMarketSimulation # (Optional, for generating paths)
# 1. Define Solver Configuration
params = SolverParams(
W_grid = [50.0, 100.0, 150.0], # Wealth grid to evaluate
poly_order = 2, # Polynomial expansion of state variables
max_taylor_order = 4, # Value function Taylor expansion order
trimming_α = 0.01 # Trim extreme 1% of paths during regression
)
# 2. Define Utility
crra(W) = (W^(1.0 - 5.0)) / (1.0 - 5.0)
utility = create_utility_from_ad(crra)
# 3. Extract Matrices from your Simulation
# Re_all: Risky Asset Excess Returns
# Z_all: Predictor State Variables
# X_all, Y_all: Risk-Free Return and Income Components
Re_all = package_excess_returns(world, ["Re_Stock"])
Z_all = get_state_variables(world, ["r"])
X_all, Y_all = create_risk_free_return_components(world, 0.0, nothing)
# 4. Solve the Dynamic Program!
policies = solve_portfolio_problem(Re_all, Z_all, X_all, Y_all, params, utility)For an explanation of the above code, see Tutorial.