重新平衡仓位
PyBroker 可用于模拟重新平衡投资组合。这意味着 PyBroker 可以模拟调整投资组合的资产配置,以匹配期望的目标配置。此外,我们的投资组合还可以通过 投资组合优化 来重新平衡,正如本文档将展示的那样。
[1]:
import pybroker as pyb
from datetime import datetime
from pybroker import ExecContext, Strategy, YFinance
pyb.enable_data_source_cache('rebalancing')
[1]:
<diskcache.core.Cache at 0x7efcf37e94c0>
等额仓位配置
Let’s assume that we want to rebalance a long-only portfolio at the beginning of every month to make sure that each stock in our portfolio has a roughly equal allocation.
首先,我们编写一个辅助函数来检测当前 K 线的日期是否为新月的开始:
[2]:
def start_of_month(ctxs: dict[str, ExecContext]) -> bool:
dt = tuple(ctxs.values())[0].dt
if dt.month != pyb.param('current_month'):
pyb.param('current_month', dt.month)
return True
return False
Next, we implement a function that will either buy or sell enough shares of a stock to reach a target allocation.
[3]:
def set_target_shares(
ctxs: dict[str, ExecContext],
targets: dict[str, float]
):
for symbol, target in targets.items():
ctx = ctxs[symbol]
target_shares = ctx.calc_target_shares(target)
pos = ctx.long_pos()
if pos is None:
ctx.buy_shares = target_shares
elif pos.shares < target_shares:
ctx.buy_shares = target_shares - pos.shares
elif pos.shares > target_shares:
ctx.sell_shares = pos.shares - target_shares
If the current allocation is above the target level, the function will sell the needed shares of the asset, while if the current allocation is below the target level, the function will buy the needed shares of the asset.
Following that, we write a rebalance
function to target each asset to an equal allocation at the beginning of every month:
[4]:
def rebalance(ctxs: dict[str, ExecContext]):
if start_of_month(ctxs):
target = 1 / len(ctxs)
set_target_shares(ctxs, {symbol: target for symbol in ctxs.keys()})
Now that we have implemented the rebalance
function, the next step is to backtest our rebalancing strategy using a portfolio of five stocks. To process all stocks at once on each bar of data, we will use the Strategy#set_after_exec method:
[5]:
strategy = Strategy(YFinance(), start_date='1/1/2018', end_date='1/1/2023')
strategy.add_execution(None, ['TSLA', 'NFLX', 'AAPL', 'NVDA', 'AMZN'])
strategy.set_after_exec(rebalance)
result = strategy.backtest()
Backtesting: 2018-01-01 00:00:00 to 2023-01-01 00:00:00
Loading bar data...
[*********************100%***********************] 5 of 5 completed
Loaded bar data: 0:00:01
Test split: 2018-01-02 00:00:00 to 2022-12-30 00:00:00
100% (1259 of 1259) |####################| Elapsed Time: 0:00:01 Time: 0:00:010000
Finished backtest: 0:00:04
[6]:
result.orders
[6]:
type | symbol | date | shares | limit_price | fill_price | fees | |
---|---|---|---|---|---|---|---|
id | |||||||
1 | buy | AAPL | 2018-01-03 | 464 | NaN | 43.31 | 0.0 |
2 | buy | AMZN | 2018-01-03 | 336 | NaN | 59.84 | 0.0 |
3 | buy | NFLX | 2018-01-03 | 99 | NaN | 203.86 | 0.0 |
4 | buy | NVDA | 2018-01-03 | 4013 | NaN | 5.22 | 0.0 |
5 | buy | TSLA | 2018-01-03 | 873 | NaN | 21.36 | 0.0 |
... | ... | ... | ... | ... | ... | ... | ... |
293 | sell | NFLX | 2022-12-02 | 15 | NaN | 315.99 | 0.0 |
294 | sell | NVDA | 2022-12-02 | 974 | NaN | 16.69 | 0.0 |
295 | buy | AAPL | 2022-12-02 | 28 | NaN | 146.82 | 0.0 |
296 | buy | AMZN | 2022-12-02 | 42 | NaN | 94.57 | 0.0 |
297 | buy | TSLA | 2022-12-02 | 70 | NaN | 193.68 | 0.0 |
297 rows × 7 columns
投资组合优化
Portfolio optimization can guide our rebalancing in order to meet some objective for our portfolio. For instance, we can use portfolio optimization with the goal of allocating stocks in a way to minimize risk.
Riskfolio-Lib 是一个用于进行投资组合优化的流行 Python 库。下面展示了如何使用它根据过去一年的收益构建一个最小风险投资组合,通过最小化投资组合的 条件风险价值(CVar):
[7]:
import pandas as pd
import riskfolio as rp
pyb.param('lookback', 252) # Use past year of returns.
def calculate_returns(ctxs: dict[str, ExecContext], lookback: int):
prices = {}
for ctx in ctxs.values():
prices[ctx.symbol] = ctx.adj_close[-lookback:]
df = pd.DataFrame(prices)
return df.pct_change().dropna()
def optimization(ctxs: dict[str, ExecContext]):
lookback = pyb.param('lookback')
if start_of_month(ctxs):
Y = calculate_returns(ctxs, lookback)
port = rp.Portfolio(returns=Y)
port.assets_stats(method_mu='hist', method_cov='hist')
w = port.optimization(
model='Classic',
rm='CVaR',
obj='MinRisk',
rf=0, # Risk free rate.
l=0, # Risk aversion factor.
hist=True # Use historical scenarios.
)
targets = {
symbol: w.T[symbol].values[0]
for symbol in ctxs.keys()
}
set_target_shares(ctxs, targets)
You can find more information and examples of using Riskfolio-Lib on its official documentation. Now, let’s move on to backtesting the strategy!
[8]:
strategy.set_after_exec(optimization)
result = strategy.backtest(warmup=pyb.param('lookback'))
Backtesting: 2018-01-01 00:00:00 to 2023-01-01 00:00:00
Loaded cached bar data.
Test split: 2018-01-02 00:00:00 to 2022-12-30 00:00:00
100% (1259 of 1259) |####################| Elapsed Time: 0:00:01 Time: 0:00:010000
Finished backtest: 0:00:01
[9]:
result.orders.head()
[9]:
type | symbol | date | shares | limit_price | fill_price | fees | |
---|---|---|---|---|---|---|---|
id | |||||||
1 | buy | AAPL | 2019-01-04 | 1420 | NaN | 36.54 | 0.0 |
2 | buy | AMZN | 2019-01-04 | 347 | NaN | 77.81 | 0.0 |
3 | buy | TSLA | 2019-01-04 | 1020 | NaN | 20.69 | 0.0 |
4 | sell | AAPL | 2019-02-04 | 103 | NaN | 42.37 | 0.0 |
5 | buy | AMZN | 2019-02-04 | 1 | NaN | 81.58 | 0.0 |
在上面,我们可以观察到,在回测的第一个月,投资组合优化将整个投资组合分配给了 5 只股票中的 3 只。