控制最大回撤:风险约束凯利准则
由bqopniu创建,最终由bqopniu 被浏览 4 用户
凯利准则对于长期交易来说已经足够好,前提是投资者对风险是中性的,并且能够承受较大的回撤。然而,在实际交易中,我们无法接受长时间和较大的回撤。为了克服凯利准则导致的较大回撤问题,Busseti等人(2016年)提出了风险约束凯利准则,它将最大化长期对数增长率与回撤作为约束结合起来。这种约束使我们能够获得更平滑的权益曲线。你将在这里了解这种新型凯利准则的一切,并将其应用于交易策略。
本文涵盖以下内容:
- 凯利准则
- 风险约束凯利准则
- 基于风险约束凯利准则的交易策略
凯利准则
凯利准则是一个著名的用于分配投资组合资源的公式。你可以在互联网上找到许多关于它的资源。例如,你可以找到凯利准则的快速定义、关于仓位大小的示例博客,甚至是关于风险管理的网络研讨会。
在这里,我们提供公式和一些基本的使用说明。
公式如下:
其中:
- K% = 凯利百分比
- W = 胜率
- R = 胜/负比率
假设我们有过去100天的策略收益。我们计算这些策略收益的胜率,并将其设置为“W”。然后我们计算正收益的绝对值除以负收益的平均值。得到的 K% 将是你下次交易的资本比例。
从理论上看,凯利准则确保了你的交易策略获得最大长期回报。然而,在实践中,如果你将该准则应用于你的交易策略,你会面临许多长期且较大的回撤。
为了解决这个问题,Busseti等人(2016年)提出了“风险约束凯利准则”,它允许我们获得更平滑的权益曲线,减少回撤的频率和幅度。
风险约束凯利准则
凯利准则与一个优化问题相关。对于风险约束版本,正如名字所示,我们增加了一个约束。约束的基本原理可以表述为:
Prob(最小财富<α)<β
回撤风险定义为 Prob(最小财富<α),其中 α∈(0,1) 是一个给定的目标(不期望的)最小财富。这种风险以一种非常复杂的方式依赖于投注向量 b。约束限制财富下降到 α 的概率不超过 β。
带有这种约束的优化问题是一个高度复杂的问题。因此,为了使其更容易解决,Busseti等人(2016年)在只有两种结果(赢和输)的情况下,提供了一个更简单的优化问题,如下所示:
其中:
- π:胜率
- P:赢的情况下的收益
- b1:要找到的凯利比例。b1=K%。这是最大化问题的控制变量
- λ:交易者的风险厌恶程度:\frac{\lg{\beta}}{\lg{\alpha}}
请注意,基本准则中定义的胜/负比率 R 为:
R=P−1
其中 P 是风险约束凯利准则中描述的赢的情况下的收益。
那如何解决优化的问题呢?
风险约束凯利准则的求解算法如下:
如果 b1=(π*P−1)/(P−1) 满足风险约束,则这就是解。否则,我们通过找到使成立的 b1 值来找到 b1。
可以通过二分法算法找到解。
基于风险约束凯利准则的交易策略
让我们来审视一个基于风险约束凯利准则的交易策略!
让我们导入所需的库。
# Import the necessary libraries
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import yfinance as yf
import math
from ta import add_all_ta_features
from sklearn import svm
让我们定义一个定制的二分法,以便后续使用:
def my_bisection(f, a, b, tol, pi, payoff, lambda_risk):
# approximates a root, R, of f bounded
# by a and b to within tolerance
# | f(m) | < tol with m the midpoint
# between a and b Recursive implementation
# check if a and b bound a root
if np.sign(f(a, pi, payoff, lambda_risk)) == np.sign(f(b, pi, payoff, lambda_risk)):
raise Exception("The scalars a and b do not bound a root")
# get midpoint
m = (a + b)/2
if np.abs(f(m, pi, payoff, lambda_risk)) < tol:
# stopping condition, report m as root
return m
elif np.sign(f(a, pi, payoff, lambda_risk)) == np.sign(f(m, pi, payoff, lambda_risk)):
# case where m is an improvement on a.
# Make recursive call with a = m
return my_bisection(f, m, b, tol, pi, payoff, lambda_risk)
elif np.sign(f(b, pi, payoff, lambda_risk)) == np.sign(f(m, pi, payoff, lambda_risk)):
# case where m is an improvement on b.
# Make recursive call with b = m
return my_bisection(f, a, m, tol, pi, payoff, lambda_risk)
让我们定义两个函数,用于计算风险约束凯利准则的投注比例:
# Define function to find the risk-constraint value
def find_constraint_value(b, pi, payoff, lambda_risk):
return pi*(b*payoff+(1-b))**(-lambda_risk) + (1-pi)*(1-b)**(-lambda_risk)-1
# Define the 2 Kelly functions inside of one
def kelly_functions(pi, payoff, lambda_risk=None):
# If there is no lambda risk
if lambda_risk is None:
# Define the basic Kelly criterion percentage value
return pi - (1-pi)/payoff
# If there is lambda risk
else:
# Set the Kelly fraction
b = (pi*payoff - 1)/(payoff - 1)
# Set the risk-constraint value
value = find_constraint_value(b, pi, payoff, lambda_risk)
# If b is nan
if math.isnan(b):
# Return is zero
return 0.0
# If b is less than zero
elif b < 0:
# Return zero
return 0.0
# If value is less than one
elif value <= 0:
# Return b
return b
# Else
else:
# Find the optimal Kelly fraction with the bisection algorithm
try:
return my_bisection(find_constraint_value, 0.01, 0.99, 1, pi, payoff, lambda_risk)
except:
return 0.0
让我们导入从1990年到2024年10月的微软(MSFT)股票数据,并计算其买入并持有的回报率:
# Download the Apple stock data
data = yf.download('AAPL', start='1990-01-01', end='2024-10-03', auto_adjust=True)
# Create the percentage returns
data['returns'] = data['Close'].pct_change()
# Output the data
data
让我们获取“ta”库中所有可用的技术指标:
# Get the input features based on the available technical indicators
features = add_all_ta_features(data, open="Open", high="High", low="Low", close="Close", volume="Volume", fillna=True).drop(\
["Open", "High", "Low", "Close", "Volume", "rets"], axis=1)
# Set 2 lists to save indicators names as needed
indicators_to_become_stationary = list()
indicators_to_drop = list()
# Loop to check for each indicator's stationarity
for indicator in features.columns.tolist():
try:
# Get the Augmented-Dickey Fuller p-value
pvalue = adfuller(features[indicator].dropna(), regression='c', autolag='AIC')[1]
# If p-value is higher than 5%
if pvalue > 0.05:
# Save the indicator name to become it stationary later
indicators_to_become_stationary.append(indicator)
# I p-value is NaN
elif np.isnan(pvalue):
# Save the indicator name to drop it later
indicators_to_drop.append(indicator)
except:
# Save the indicator name to drop it later
indicators_to_drop.append(indicator)
# Make the respective indicators stationary
features[indicators_to_become_stationary] = features[indicators_to_become_stationary].pct_change()
# Drop the respective indicators which p-values where NaN
features.drop(indicators_to_drop, axis=1, inplace=True)
# Concatenate the input features with the previous dataframe
data = pd.concat([data, features], axis=1)
# Set the features columns' names as a list
features = features.columns.tolist()
让我们创建预测特征以及一些相关的列:
# Set the prediction feature
data['y'] = np.where(data['returns'].shift(-1)>=0, 1.0, 0.0)
# Set the signal column
data['signal'] = 0.0
# Set the train_signal column
data['train_signal'] = 0.0
# Set the basic Kelly criterion position size column
data['pos_size'] = 0.0
# Drop the NaN values
data.dropna(inplace=True)
# Output the data
data
让我们定义随机种子以及一些其他相关的变量:
# Set the random seed
np.random.seed(100)
# Set the window to be used to subset the data
window = 250
# Set the test span to be used to split the data to fit the ML model
test_span = 60
# Set the lambda risk for the risk-constraint Kelly criterion
lambda_risk = 5.7
# Set the initial payoff
payoff = 0.0
我们将使用一个for循环来遍历每一个日期。
算法的流程如下,对于每一天:
- 从数据中抽取子样本,使用一年的数据,并将最后60天作为子样本数据的测试区间。
- 将数据拆分为特征 X 和目标 y,以及它们各自的训练集和测试集。
- 拟合一个支持向量机模型。
- 预测信号。
- 获得策略收益。
- 计算正收益的平均值,记为pos_avg
- 计算负收益的平均值,记为neg_avg
- 计算正收益的数量,记为pos_ret_num
- 计算负收益的数量,记为neg_ret_num
- 设置一些条件以确定当天的仓位大小。
- 计算基础凯利比例和风险约束凯利比例。
- 再次拆分数据为训练集和测试集,以:
- 重新估计模型,
- 预测下一天的信号。
# Set the for loop
for t in range(window,len(data.index)):
# Set the data sample
data_sample = data.iloc[(t-window):t,:].copy()
# Set the X train data
X = data_sample.loc[data_sample.index[:(-1-test_span)],features]
# Set the X test data
X_test = data_sample.loc[data_sample.index[(-test_span):],features]
# Set the prediction feature train data
y = data_sample.loc[data_sample.index[:(-1-test_span)],'y']
# Set the ML model
clf = svm.LinearSVC()
# Fit the model
clf.fit(X, y)
# Compute the train-sample predictions
data_sample.loc[data_sample.index[:(-1-test_span)],'train_signal'] = clf.predict(X)
# Compute the train-data strategy returns
data_sample['train_stra_returns'] = data_sample['returns'] * data_sample['train_signal'].shift(1)
# Set the strategy mean positive return
pos_avg = data_sample[data_sample['train_stra_returns']>0]['train_stra_returns'].iloc[(-test_span):].mean()
# Set the strategy mean negative return
neg_avg = abs(data_sample[data_sample['train_stra_returns']<0]['train_stra_returns'].iloc[(-test_span):].mean())
# Set the total number of positive returns
pos_ret_num = len(data_sample[data_sample['train_stra_returns']>0].index[(-test_span):])
# Set the total number of negative returns
neg_ret_num = len(data_sample[data_sample['train_stra_returns']<0].index[(-test_span):])
# If the sum of both total number of positive and negative returns is higher than zero
if (pos_ret_num + neg_ret_num) > 0:
# Compute the winning probability
pi = pos_ret_num / (pos_ret_num + neg_ret_num)
# If the mean negative return is higher than zero
if neg_avg > 0:
# Compute the payoff
payoff = (1+pos_avg/neg_avg)
# Compute the risk-constraint Kelly fraction
data.loc[data.index[t],'rc_kelly_pos_size'] = np.round(kelly_functions(pi, payoff, lambda_risk),3)
# Set the basic Kelly fraction
data.loc[data.index[t],'kelly_pos_size'] = np.round(kelly_functions(pi, payoff),3)
# If the mean negative return is equal or lower than zero
else:
# Set the risk-constraint Kelly fraction as zero
data.loc[data.index[t],'rc_kelly_pos_size'] = 0
# Set the basic Kelly fraction
data.loc[data.index[t],'kelly_pos_size'] = 0
# If the sum of both the total number of positive and negative returns is equal or lower than zero
else:
# Set the risk-constraint Kelly fraction as zero
data.loc[data.index[t],'rc_kelly_pos_size'] = 0
# Set the basic Kelly fraction
data.loc[data.index[t],'kelly_pos_size'] = 0
# Set the new X train data
X = data_sample.loc[data_sample.index[:-1],features]
# Set the new X test data
X_test = data_sample.loc[data_sample.index[-1:],features]
# Set the new train-data prediction feature
y = data_sample.loc[data_sample.index[:-1],'y']
# Create the ML object
clf = svm.LinearSVC()
# Fit the model
clf.fit(X, y)
# Predict the signal and save it
data.loc[data.index[t],'signal'] = clf.predict(X_test)
让我们计算策略收益。我们计算了两种策略:基础凯利策略和风险约束凯利策略。除此之外,我还引入了一个“改进版”的策略,它与前两种策略的信号相同,但增加了一个条件:只有当买入并持有的累积收益高于其30天移动平均值时,才执行交易信号。
# Subset the data to create the strategy returns
results = data.iloc[window:,:]
# Create the Buy-and-Hold cumulative returns
results['bh_cum_rets'] = (1+results['returns']).cumprod()
# Create the risk-constraint-Kelly-based strategy returns
results['rc_kelly_rets'] = results['returns'] * results['signal'].shift() * results['rc_kelly_pos_size'].shift()
# Create the risk-constraint-Kelly-based strategy cumulative returns
results['rc_kelly_cum_rets'] = (1+results['rc_kelly_rets']).cumprod()
# Create the basic-Kelly-based strategy returns
results['kelly_rets'] = results['returns'] * results['signal'].shift() * results['kelly_pos_size'].shift()
# Create the basic-Kelly-based strategy cumulative returns
results['kelly_cum_rets'] = (1+results['kelly_rets']).cumprod()
# Create a moving average signal
results['mov_avg_signal'] = np.where(results['bh_cum_rets'] > results['bh_cum_rets'].rolling(17).mean(),1,0)
# Create the improved risk-constraint-Kelly-based strategy returns
results['rc_kelly_rets_imp'] = results['rc_kelly_rets'] * results['mov_avg_signal'].shift()
# Create the improved basic-Kelly-based strategy returns
results['kelly_rets_imp'] = results['kelly_rets'] * results['mov_avg_signal'].shift()
# Create the improved risk-constraint-Kelly-based strategy cumulative returns
results['rc_kelly_cum_rets_imp'] = (1+results['rc_kelly_rets_imp']).cumprod()
# Create the improved basic-Kelly-based strategy cumulative returns
results['kelly_cum_rets_imp'] = (1+results['kelly_rets_imp']).cumprod()
让我们现在来看一下图表。我们看到的是基础凯利策略的仓位大小:
# Set the figure size
plt.figure(figsize=(15,7))
# Plot both the Buy-and-hold and the strategies' cumulative returns
plt.plot(results.index, results['kelly_pos_size'], label = "Position sizes")
# Set the title of the graph
plt.title('Kelly position sizes', fontsize=16)
# Set the x- and y- axis labels and ticks sizes
plt.xlabel('Year', fontsize=15)
plt.ylabel('Position sizes', fontsize=15)
plt.tick_params(axis='both', labelsize=15)
# Set the plot legend location
plt.legend(loc=2, prop={'size': 15}, bbox_to_anchor=(0,1))
plt.savefig('Figures/basic-kelly-position-size.png', bbox_inches='tight')
它的波动性很高,范围从0到0.6。让我们来看一下风险约束凯利比例:
# Set the figure size
plt.figure(figsize=(15,7))
# Plot both the Buy-and-hold and the strategies' cumulative returns
plt.plot(results.index, results['rc_kelly_pos_size'], label = "Position sizes")
# Set the title of the graph
plt.title('Risk-constraint Kelly position sizes', fontsize=16)
# Set the x- and y- axis labels and ticks sizes
plt.xlabel('Year', fontsize=15)
plt.ylabel('Position sizes', fontsize=15)
plt.tick_params(axis='both', labelsize=15)
# Set the plot legend location
plt.legend(loc=2, prop={'size': 15}, bbox_to_anchor=(0,1))
plt.savefig('Figures/rc-kelly-position-size.png', bbox_inches='tight')
它的波动范围现在是从0到0.25,波动性较低。让我们来看一下这两种策略的收益情况。
# Set the figure size
plt.figure(figsize=(15,7))
# Plot both the Buy-and-hold and the strategies' cumulative returns
plt.plot(results.index, results['rc_kelly_cum_rets'], label = "Risk-constraint Kelly strategy")
plt.plot(results.index, results['kelly_cum_rets'], label = "Basic Kelly strategy")
# Set the title of the graph
plt.title('Buy-and-Hold, Kelly and risk-constraint Kelly based strategies', fontsize=16)
# Set the x- and y- axis labels and ticks sizes
plt.xlabel('Year', fontsize=15)
plt.ylabel('Cumulative Returns', fontsize=15)
plt.tick_params(axis='both', labelsize=15)
# Set the plot legend location
plt.legend(loc=2, prop={'size': 15}, bbox_to_anchor=(0,1))
plt.savefig('Figures/strategies_cum_returns.png', bbox_inches='tight')
基础凯利策略的最大回撤较高,这在非正式检查中已经得到确认。风险约束凯利策略的主要缺点是权益曲线较低。
让我们来看一下改进策略的收益情况。
# Set the figure size
plt.figure(figsize=(15,7))
# Plot both the Buy-and-hold and the strategies' cumulative returns
plt.plot(results.index, results['rc_kelly_cum_rets'], label = "Risk-constraint Kelly strategy")
plt.plot(results.index, results['kelly_cum_rets'], label = "Basic Kelly strategy")
# Set the title of the graph
plt.title('Buy-and-Hold, Kelly and risk-constraint Kelly based strategies', fontsize=16)
# Set the x- and y- axis labels and ticks sizes
plt.xlabel('Year', fontsize=15)
plt.ylabel('Cumulative Returns', fontsize=15)
plt.tick_params(axis='both', labelsize=15)
# Set the plot legend location
plt.legend(loc=2, prop={'size': 15}, bbox_to_anchor=(0,1))
plt.savefig('Figures/strategies_cum_returns.png', bbox_inches='tight')
有趣的是,基础凯利策略和风险约束策略的回撤都得到了减少。然而,风险约束策略的权益曲线仍然较低。
以下是一些评论:
- 当你拥有一个良好的夏普比率时,你可以增加杠杆率。因此,不要因为风险约束凯利策略的低权益曲线而感到失望。我将检查这一点作为一项练习。
- 你可以通过设置止损和获利目标来提高权益收益。
- 你可以将风险约束凯利准则与元标签(meta-labelling)结合起来。
- 风险约束凯利准则的局限性在于低权益曲线。
- 你可以使用库来实现交易总结统计和分析,以正式验证风险约束凯利策略的较低回撤和波动率。
结论
正如你所看到的,你可以实施风险约束凯利准则来获得更平滑的权益曲线。主要问题可能是它会让你的累积回报较低,但它可以帮助你找到不需要交易的日子,从而避免回撤!
\