AI量化知识树

控制最大回撤:风险约束凯利准则

由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循环来遍历每一个日期。

算法的流程如下,对于每一天:

  1. 从数据中抽取子样本,使用一年的数据,并将最后60天作为子样本数据的测试区间。
  2. 将数据拆分为特征 X 和目标 y,以及它们各自的训练集和测试集。
  3. 拟合一个支持向量机模型。
  4. 预测信号。
  5. 获得策略收益。
  6. 计算正收益的平均值,记为pos_avg
  7. 计算负收益的平均值,记为neg_avg
  8. 计算正收益的数量,记为pos_ret_num
  9. 计算负收益的数量,记为neg_ret_num
  10. 设置一些条件以确定当天的仓位大小。
  11. 计算基础凯利比例和风险约束凯利比例。
  12. 再次拆分数据为训练集和测试集,以:
    • 重新估计模型,
    • 预测下一天的信号。
# 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')

有趣的是,基础凯利策略和风险约束策略的回撤都得到了减少。然而,风险约束策略的权益曲线仍然较低。

以下是一些评论:

  1. 当你拥有一个良好的夏普比率时,你可以增加杠杆率。因此,不要因为风险约束凯利策略的低权益曲线而感到失望。我将检查这一点作为一项练习。
  2. 你可以通过设置止损和获利目标来提高权益收益
  3. 你可以将风险约束凯利准则与元标签(meta-labelling)结合起来
  4. 风险约束凯利准则的局限性在于低权益曲线
  5. 你可以使用库来实现交易总结统计和分析,以正式验证风险约束凯利策略的较低回撤和波动率

结论

正如你所看到的,你可以实施风险约束凯利准则来获得更平滑的权益曲线。主要问题可能是它会让你的累积回报较低,但它可以帮助你找到不需要交易的日子,从而避免回撤!



\

标签

交易策略风险管理资产配置
{link}