from vectorbtpro import *
from datetime import datetime
from dateutil.relativedelta import relativedelta
from pypfopt.objective_functions import transaction_cost
from pypfopt.expected_returns import mean_historical_return
from pypfopt.risk_models import CovarianceShrinkage
from pypfopt.efficient_frontier import EfficientFrontier
vbt.settings.set_theme("dark")
Portfolio Optimization¶
We will be using vector-bt, a (semi-)professional vectorized backtesting library written in Python, written by Oleg Polakow.
Note: For detailed documentation see documentation
Financial markets are highly nondeterministic. We want to find the portfolio that offers us high returns at low risk under the following market assumptions.
- Rational decision-makers: Investors want to maximise return while reducing the risks associated with their investment
- No arbitrage : cannot make a costless, riskless profit
- Risky securities : $\{S_1,\ldots,S_n\}:\, n\geq 2$ be risky securities whose future returns are uncertain. There is no risk-free asset $S_0$ in our portfolio
- Equilibrium : supply equals demand for securities
- Liquidity : any # of units of a security can be bought and sold quickly
- Access to information : rapid availability of accurate information
- Price is efficient : Price of security adjusts immediately to new information, and current price reflects past information and expected further behaviour
- No transaction costs and taxes : transaction costs are assumed to be negligible compared to value of trades (we may relax this assumption and assume cost $K$ per transaction) and are ignored. No taxes (capital-gains etc.) on transactions.
We want to select a portfolio $\mathbf{x}=(x_1,\ldots,x_n)\in\mathbb{R}^n$ where $x_i$ is the amount (fraction of budget) invested in security $S^i$. We work in discrete-time, so we only consider now ($t=0$) and a moment in time in the future ($t=1$)
The return of asset $i$ is a random variable $r_i:\Omega \rightarrow [0,\infty)$ where $\Omega$ is the sample space (set of scenarios for the future). If $S_t^i:\Omega\rightarrow\mathbb{R}_+$ is the random variable denoting the future value of asset $i$ at time $t$. Then under scenario $\omega\in\Omega$, we have that $r_i(\omega)=S^i_1(\omega)/S_0^i$. So for instance, a $5\%$ return on asset $i$ corresponds to $r_i=1.05$ and $-2%$ return corresponds to $r_i=0.98$
$\newcommand{\x}{\mathbf{x}}$ Let $r = (r_1,\ldots, r_n)$ denote the random vector of returns with mean $\mu=\mathbf{E}[r]\in\mathbb{R}^n$ and covariance matrix $\Sigma\in\mathbb{R}^{n\times n}$. Let the set of admissible portfolios be denoted by $X=\{\x\in\mathbb{R}^n:\sum_{i=0}^n x_i=1,\, \x\geq 0\}\subset\mathbb{R}^n$. In other words, we do not allow short-selling (for now), and must always be fully-allocated.
Then the return of portfolio $\mathcal{R}:X\rightarrow \mathbb{R}$ is given by $\mathcal{R}(\x)=r^T\x \implies \mathbf{E}[\mathcal{R}] = \mu^T\x$.
The risk of portfolio $\x\in X$ is denoted $\text{Risk}(\x)\equiv\text{Risk}(\mathcal{R}(\x))$ where $\text{Risk}:X\rightarrow\mathbb{R}$ is the risk measure.
Data Fetching¶
END_DATE = datetime.today().strftime("%Y-%m-%d")
START_DATE = (datetime.today() - relativedelta(years=3)).strftime("%Y-%m-%d")
data = vbt.YFData.pull(
["BTC-USD", "ETH-USD", "XMR-USD", "LINK-USD"],
start=f"{START_DATE} UTC",
end=f"{END_DATE} UTC",
timeframe="1d"
)
#data.to_hdf("data/BinanceData.h5") # save data to disk
#data = vbt.HDFData.pull("data/BinanceData.h5") # retrieve data from disk
The object returned inherits from the vectorbtpro.data base class, which is very extensible, but for modeling purposes we only really care about OHLC data.
df = data.get("close")
df.tail()
| symbol | BTC-USD | ETH-USD | XMR-USD | LINK-USD |
|---|---|---|---|---|
| Date | ||||
| 2026-02-21 00:00:00+00:00 | 68003.765625 | 1973.737183 | 326.570953 | 8.876016 |
| 2026-02-22 00:00:00+00:00 | 67659.390625 | 1957.805908 | 327.830200 | 8.672669 |
| 2026-02-23 00:00:00+00:00 | 64616.738281 | 1855.502808 | 307.265533 | 8.265229 |
| 2026-02-24 00:00:00+00:00 | 64080.042969 | 1852.969482 | 321.333679 | 8.203102 |
| 2026-02-25 00:00:00+00:00 | 67960.125000 | 2054.627930 | 344.504822 | 9.256296 |
Optimization¶
$\newcommand{\x}{\mathbf{x}}$ $\newcommand{\R}{\mathbb{R}}$
We want to maximize expected return while minimizing risk

Here we consider the risk-return trade-off:$\quad \max\; \{\mu^T\x -\delta\text{Risk}(x)\, |\, \x\in X \}\quad \text{where}\quad X=\{\x\in(-1,1)^n\, |\, \x^T\mathbf{1}=1 \}$
def mean_var(x, mu, cov_matrix, delta):
var = x.T @ (cov_matrix * np.eye(cov_matrix.shape[0])) @ x
return -((mu.T @ x) - delta * var)
def optimize_func(price, index_slice):
price_filt = price.iloc[index_slice]
mu = mean_historical_return(price_filt)
S = CovarianceShrinkage(price_filt).ledoit_wolf()
ef = EfficientFrontier(mu, S, weight_bounds=(0.01, 0.3))
ef.convex_objective(mean_var, mu=mu.values, cov_matrix=S.values, delta=0.1)
weights = ef.clean_weights()
return weights
pfo = vbt.PortfolioOptimizer.from_optimize_func(
data.symbol_wrapper,
optimize_func,
data.get("Close"),
vbt.Rep("index_slice"),
every="M"
)
pf = pfo.simulate(data, freq="1d")
pf.stats()
Start Index 2023-02-26 00:00:00+00:00 End Index 2026-02-25 00:00:00+00:00 Total Duration 1096 days 00:00:00 Start Value 100.0 Min Value 85.068166 Max Value 376.818576 End Value 222.172847 Total Return [%] 122.172847 Benchmark Return [%] 91.198686 Position Coverage [%] 96.89781 Max Gross Exposure [%] 100.0 Max Drawdown [%] 47.942168 Max Drawdown Duration 256 days 00:00:00 Total Orders 140 Total Fees Paid 0.0 Total Trades 71 Win Rate [%] 83.58209 Best Trade [%] 109.456133 Worst Trade [%] -23.995042 Avg Winning Trade [%] 39.721564 Avg Losing Trade [%] -9.148827 Avg Winning Trade Duration 556 days 20:34:17.142857144 Avg Losing Trade Duration 418 days 10:54:32.727272728 Profit Factor 8.428469 Expectancy 2.221654 Sharpe Ratio 0.76592 Calmar Ratio 0.635229 Omega Ratio 1.120244 Sortino Ratio 1.087971 Name: group, dtype: object
$\newcommand{\x}{\mathbf{x}}$ We choose to maximize the sharpe ratio which $S_a=\mathbf{E}[R^i-R^0]/\sqrt{\mathbf{V}[R^i-R^0]}$ where $R^0$ is the risk-free asset return (like the U.S treasury bond - it has essentially no risk - backed by US military / country's stability, this could also be the interest gained by making a bank deposit) and $R^i$ is asset $i$'s return. The portfolio with maximum sharpe ratio is the tangency portfolio.
A portfolio is efficient if it has the maximum expected return among all admissible portfolios of the same (or smaller) risk: $\max \{\mu^T\x : \text{Risk}(\x)\leq \sigma^2,\, \x\in X\}$ or if it has the minimum variance among all admissible portfolios of the same (or greater) expected return: $\min\{ \text{Risk}(\x) : \mu^T\x\geq R,\, x\in X\}$
pfo1 = vbt.PortfolioOptimizer.from_pypfopt(
prices=data.get("Close"),
every="1M",
weight_bounds=(0, 0.8),
target=vbt.Param([
"max_sharpe",
"min_volatility",
"max_quadratic_utility"
# optimize for a set of targets separately
# for more target parameters see: https://vectorbt.pro/pvt_74d2a4ff/tutorials/portfolio-optimization/integrations/#periodically
])
)
# get portfolio performance for each target
pf1 = pfo1.simulate(data, freq="1d")
print(f"{pf1.sharpe_ratio}\n\n{pf1.max_drawdown}\n\n{pf1.annualized_return}")
target max_sharpe 1.241508 min_volatility 0.832098 max_quadratic_utility 1.001517 Name: sharpe_ratio, dtype: float64 target max_sharpe -0.549641 min_volatility -0.426625 max_quadratic_utility -0.566519 Name: max_drawdown, dtype: float64 target max_sharpe 0.793812 min_volatility 0.321258 max_quadratic_utility 0.551526 Name: annualized_return, dtype: float64
fig = vbt.make_subplots(rows=1, cols=2)
fig.update_layout(width=1000,height=400)
pf1.plot_cumulative_returns(column=1, add_trace_kwargs=dict(row=1, col=1), fig=fig)
pfo1.plot(column=1, add_trace_kwargs=dict(row=1, col=2), fig=fig)
So the Sharpe ratio $S_a=1.24$ is not ideal if we're chasing alpha (i.e. profitable strategy whether the market is bearish/bullish), but if we're just looking to increase our beta (increasing exposure to the market swings to capture the most price action) then this is starting to look better.
initial_weights = np.array([1 / len(data.symbols)] * len(data.symbols))
pfo2 = vbt.PortfolioOptimizer.from_pypfopt(
prices=data.get("Close"),
every="1M",
weight_bounds=(-1, 1),
objectives=["transaction_cost"],
w_prev=initial_weights,
k=0.001,
target=vbt.Param([
"max_sharpe",
"min_volatility",
"max_quadratic_utility"
])
)
pf2 = pfo2.simulate(data, freq="1d")
print(f"{pf2.sharpe_ratio}\n\n{pf2.max_drawdown}\n\n{pf2.annualized_return}")
target max_sharpe 0.577411 min_volatility 0.716948 max_quadratic_utility 1.337973 Name: sharpe_ratio, dtype: float64 target max_sharpe -0.409129 min_volatility -0.342790 max_quadratic_utility -0.407796 Name: max_drawdown, dtype: float64 target max_sharpe 0.176048 min_volatility 0.235928 max_quadratic_utility 0.719952 Name: annualized_return, dtype: float64
If we consider a per-trade transction cost of $0.1\%$ of the amount traded, the story changes; we now are actually less profitable with an atrocious decrease of $> 60\%$ in annualized returns (for a max_sharpe optimization objective).
However, in optimizing for minimum volatility, including transaction fees only shifts the annualized returns down by about $10\%$.
Conclusion¶
Optimizing portfolios for certain target metrics periodically and rebalancing can lead to promising results for beta-seeking strategies. However this all depends on the optimization constraints, the target parameter, and of course the assets considered.
For those of you interested, there is web-app service called Portfolio Visualizer where you can optimize portfolios subject to different constraints.
Please do not distribute this notebook.
The analysis in this material is provided for information only and should not be construed as advice to buy any cryptocurrency