核心前提:
- CTA策略已存在: 我们假设你已经有了一个(或多个)CTA策略,并通过历史回测得到了一系列的交易记录(盈利或亏损)。
- 数据准备: 关键是获取可靠的
p
(胜率) 和b
(赔率/盈亏比)。
场景一:基于固定历史数据的基本分数凯利
这是最基础的应用。策略回测完成后,基于整个回测期的表现计算一次凯利比例,并使用一个固定的折扣因子(分数凯利)。
Python 示例:
import numpy as np
import pandas as pd
# 假设这是你的策略回测交易记录 (PnL: Profit and Loss)
# trade_results = [-100, 250, -50, 300, -120, 400, -80] # 示例数据
# 更真实的数据可能来自 Pandas DataFrame
trades_df = pd.DataFrame({
'pnl': [-100, 250, -50, 300, -120, 400, -80, 150, -60, 220]
})
def calculate_kelly_basic(trade_pnl_series, kelly_fraction=0.5):
"""
基于历史交易盈亏计算基础凯利比例 (带折扣因子)
Args:
trade_pnl_series (pd.Series): 包含单笔交易盈亏的 Pandas Series
kelly_fraction (float): 凯利折扣因子 (0 < k <= 1), 推荐远小于1
Returns:
float: 推荐的资金使用比例 (分数凯利 f)
返回 -1 表示无法计算或期望为负
"""
n_trades = len(trade_pnl_series)
if n_trades == 0:
print("没有交易记录,无法计算凯利比例。")
return -1
wins = trade_pnl_series[trade_pnl_series > 0]
losses = trade_pnl_series[trade_pnl_series <= 0] # 包含0 PnL视为亏损或不计
n_wins = len(wins)
n_losses = n_trades - n_wins
if n_losses == 0: # 胜率100% (理论情况)
p = 1.0
# 无法计算赔率b,可以认为风险极小,但 Full Kelly 仍有风险
# 实际中,可以设定一个最大允许比例或基于波动性决定
print("警告: 胜率100%,无法计算传统赔率。返回设定的最大允许比例或分数。")
return kelly_fraction # 或者一个预设的上限,如 0.1
if n_wins == 0: # 胜率0%
print("胜率为0,不建议投入资金。")
return 0.0
# 计算胜率 p
p = n_wins / n_trades
# 计算平均盈利和平均亏损
avg_win = wins.mean()
avg_loss = abs(losses.mean()) # 注意取绝对值
if avg_loss == 0: # 防止除以零
print("警告: 平均亏损为0,无法计算赔率。")
# 类似于胜率100%的情况处理
return kelly_fraction
# 计算赔率 b
b = avg_win / avg_loss
# 计算 Full Kelly f*
q = 1 - p
f_star = p - q / b
if f_star <= 0:
print(f"计算得到的凯利比例 f* = {f_star:.4f} <= 0。策略期望为负或零,不建议投入资金。")
return 0.0
else:
# 应用分数凯利
f = kelly_fraction * f_star
print(f"胜率 p: {p:.4f}")
print(f"赔率 b: {b:.4f}")
print(f"全凯利 f*: {f_star:.4f}")
print(f"使用分数 k = {kelly_fraction}, 最终凯利比例 f: {f:.4f}")
return f
# --- 使用示例 ---
account_equity = 1_000_000 # 假设账户总权益
kelly_k = 0.2 # 使用 20% 的凯利比例 (比较保守)
# 计算凯利比例 f
kelly_f = calculate_kelly_basic(trades_df['pnl'], kelly_fraction=kelly_k)
if kelly_f > 0:
# 这个 f 代表你这次交易愿意承担风险的资金占总权益的比例
# 这里的 "风险" 通常指预期亏损 (基于平均亏损或止损点)
risk_capital_per_trade = account_equity * kelly_f
print(f"\n根据凯利公式 (k={kelly_k}), 单次交易目标风险资金: {risk_capital_per_trade:,.2f}")
# 后续需要结合波动率或固定止损来计算具体头寸大小 (见场景三)
else:
print("\n凯利计算结果不建议投入资金。")
说明:
- 输入: 函数需要一个包含历史交易盈亏记录的Pandas Series。
- 计算: 分别计算胜率
p
和 赔率b
。 - Full Kelly: 计算
f* = p - (1-p)/b
。 - 检查: 如果
f* <= 0
,说明策略期望值为负或零,不应投入资金。 - 分数凯利: 将
f*
乘以一个小于1的kelly_fraction
(即k
),得到最终的头寸比例f
。 - 输出: 返回计算出的分数凯利比例
f
。这个f
代表的是风险预算比例。
场景二:动态凯利 - 使用滚动窗口估计参数
考虑到市场条件会变化,使用固定历史数据可能不准确。采用滚动窗口来动态估计 p
和 b
更能适应近期市场。
Python 示例:
import pandas as pd
# 假设 trades_df 包含时间戳和 pnl
# trades_df = pd.DataFrame({'timestamp': pd.to_datetime([...]), 'pnl': [...]})
# trades_df = trades_df.set_index('timestamp') # 假设时间为索引
# 为了演示,我们继续用之前的 trades_df,并假设它是按时间顺序的
trades_df['timestamp'] = pd.to_datetime(pd.date_range(start='2023-01-01', periods=len(trades_df), freq='D'))
trades_df = trades_df.set_index('timestamp')
def calculate_kelly_rolling(trade_pnl_series, window_size, kelly_fraction=0.5):
"""
使用滚动窗口动态计算凯利比例
Args:
trade_pnl_series (pd.Series): 时间索引的交易盈亏 Series
window_size (int): 滚动窗口大小 (交易次数)
kelly_fraction (float): 凯利折扣因子
Returns:
pd.Series: 每个时间点计算出的分数凯利比例 f
"""
# 定义一个函数,用于在滚动窗口上计算凯利比例
def rolling_kelly_func(window_pnl):
# 复用之前的基础凯利计算逻辑 (稍作修改以适应滚动应用)
n_trades = len(window_pnl)
if n_trades < window_size / 2: # 窗口内数据太少,不可靠
return np.nan # 或返回 0
wins = window_pnl[window_pnl > 0]
losses = window_pnl[window_pnl <= 0]
n_wins = len(wins)
n_losses = n_trades - n_wins
if n_losses == 0 or n_wins == 0 or losses.mean() == 0:
# 边界情况,返回0或nan表示无法计算或不交易
return np.nan # 或 0.0
p = n_wins / n_trades
avg_win = wins.mean()
avg_loss = abs(losses.mean())
b = avg_win / avg_loss
q = 1 - p
f_star = p - q / b
if f_star <= 0:
return 0.0
else:
return kelly_fraction * f_star
# 应用滚动计算
# min_periods 确保窗口内有足够的数据点才开始计算
rolling_kelly_f = trade_pnl_series.rolling(window=window_size, min_periods=max(10, window_size // 2)).apply(rolling_kelly_func, raw=True)
return rolling_kelly_f
# --- 使用示例 ---
rolling_window = 50 # 使用过去50笔交易计算
kelly_k = 0.2
# 假设我们有更长的历史数据用于滚动计算
# 这里用 trades_df 只是演示,实际需要更多数据
if len(trades_df) >= rolling_window:
dynamic_kelly_f = calculate_kelly_rolling(trades_df['pnl'], window_size=rolling_window, kelly_fraction=kelly_k)
print("\n动态凯利比例 (滚动窗口):")
print(dynamic_kelly_f.tail()) # 显示最后几个计算结果
# 在实际交易中,你会使用最新的 dynamic_kelly_f 值来决定下一笔交易的风险预算
latest_f = dynamic_kelly_f.iloc[-1] if not pd.isna(dynamic_kelly_f.iloc[-1]) else 0
print(f"\n最新的动态凯利比例 f: {latest_f:.4f}")
if latest_f > 0:
risk_capital_per_trade_dynamic = account_equity * latest_f
print(f"根据最新的动态凯利 (k={kelly_k}, window={rolling_window}), 单次交易目标风险资金: {risk_capital_per_trade_dynamic:,.2f}")
else:
print("最新的动态凯利计算结果不建议投入资金。")
else:
print(f"\n交易数据不足 {rolling_window} 条,无法进行滚动计算。")
说明:
- 滚动窗口: 使用 Pandas 的
rolling()
方法,指定一个窗口大小(例如过去50笔交易)。 - 应用函数: 对每个滚动窗口应用我们定义的
rolling_kelly_func
函数来计算该窗口内的p
,b
, 和f
。 - 动态比例: 这样会得到一个随时间变化的凯利比例序列。在进行下一笔交易时,使用最新的有效凯利比例。
min_periods
: 确保窗口内至少有一定数量的交易才开始计算,避免早期数据不足导致结果不稳定。
场景三:凯利结合波动率调整 (实际头寸计算)
这是CTA中非常常见和实用的方法。凯利公式确定总风险预算(占总权益的百分比 f
),然后根据标的资产的波动率来计算具体的头寸规模(合约数量),使得单次交易的最大预期损失(通常基于ATR止损)大致等于这个风险预算。
Python 示例:
import math
def calculate_position_size_kelly_vol(account_equity, kelly_f, atr, point_value, atr_stop_multiplier=2.0):
"""
结合凯利比例和ATR波动率计算头寸规模 (期货合约数量)
Args:
account_equity (float): 当前账户总权益
kelly_f (float): 由凯利公式计算出的风险预算比例 (分数凯利 f)
atr (float): 标的资产的当前 ATR (Average True Range) 值
point_value (float): 每张合约的价格乘数 (例如,IF沪深300是 300)
atr_stop_multiplier (float): 使用几倍ATR作为止损距离
Returns:
int: 建议的交易合约数量 (向下取整)
"""
if kelly_f <= 0:
print("凯利比例为0或负数,不开仓。")
return 0
if atr <= 0 or point_value <= 0:
print("ATR或Point Value无效,无法计算头寸。")
return 0
# 1. 计算单次交易的目标风险金额
target_risk_amount = account_equity * kelly_f
# 2. 计算单张合约的风险金额 (基于ATR止损)
# 假设止损设置在入场点 +/- atr_stop_multiplier * ATR 处
risk_per_contract = atr * atr_stop_multiplier * point_value
if risk_per_contract == 0:
print("单张合约风险为0 (ATR可能为0),无法计算头寸。")
return 0
# 3. 计算理论头寸数量
theoretical_position_size = target_risk_amount / risk_per_contract
# 4. 向下取整得到实际可交易的合约数量
position_size = math.floor(theoretical_position_size)
print(f"账户权益: {account_equity:,.2f}")
print(f"凯利风险预算比例 f: {kelly_f:.4f}")
print(f"单次交易目标风险金额: {target_risk_amount:,.2f}")
print(f"ATR: {atr:.2f}, ATR止损倍数: {atr_stop_multiplier}, 合约乘数: {point_value}")
print(f"单张合约风险 (基于ATR止损): {risk_per_contract:,.2f}")
print(f"理论头寸数量: {theoretical_position_size:.4f}")
print(f"实际头寸数量 (向下取整): {position_size}")
return position_size
# --- 使用示例 ---
account_equity = 1_000_000
# 假设从场景一或场景二得到 kelly_f
# kelly_f = calculate_kelly_basic(trades_df['pnl'], kelly_fraction=0.2)
# 或者使用最新的动态凯利比例
latest_f = 0.08 # 假设最新的动态凯利 f 为 0.08 (8%)
# 假设交易 沪深300股指期货 (IF)
current_atr_IF = 35.5 # 当前IF合约的ATR值 (示例)
point_value_IF = 300 # IF合约的乘数
# 计算应交易的合约数量
contracts_to_trade = calculate_position_size_kelly_vol(
account_equity,
latest_f,
current_atr_IF,
point_value_IF,
atr_stop_multiplier=2.0 # 使用2倍ATR止损
)
print(f"\n建议交易 IF 合约数量: {contracts_to_trade}")
说明:
- 风险预算: 首先,根据凯利比例
f
和总权益account_equity
计算出这次交易愿意承担的最大风险金额target_risk_amount
。 - 单合约风险: 然后,基于当前市场的波动率(常用ATR)和预设的止损逻辑(例如离入场价
atr_stop_multiplier
倍ATR处止损),计算出交易一张合约会带来的预期风险risk_per_contract
。 - 头寸计算: 用总风险预算除以单张合约的风险,得到理论上可以持有的合约数量。
- 取整: 因为合约不能分割,所以向下取整得到最终的整数合约数量。
场景四:多策略/多市场组合(简化视角)
当CTA系统包含多个子策略或交易多个不相关(或低相关)的市场时,直接应用单一凯利比例可能不是最优的。理论上需要使用更复杂的组合凯利公式(涉及协方差矩阵)。实践中,常用简化方法:
- 方法A:独立计算,风险预算分配:
- 为每个子策略/市场独立计算其凯利比例
f_i
(可以使用场景一或二)。 - 根据某种规则(如等权重、基于夏普比率加权、基于
f_i
大小加权)为每个子策略/市场分配总风险预算的一部分。 - 在每个子策略/市场内部,使用分配到的风险预算和其自身的波动率(场景三)来计算头寸。
- 为每个子策略/市场独立计算其凯利比例
- 方法B:风险平价思想:
- 设定一个总的投资组合风险目标(如目标年化波动率或最大回撤)。
- 估计每个策略/市场的预期波动率和它们之间的相关性。
- 分配风险预算,使得每个策略/市场对总组合的边际风险贡献大致相等(或符合预设比例)。这通常需要优化算法。
Python 示例 (方法A - 简化演示):
# 假设有两个策略/市场的数据
trades_pnl_strategy1 = pd.Series([-50, 150, -30, 200, -80])
trades_pnl_strategy2 = pd.Series([80, -40, 120, -60, 100, -50])
atr_market1 = 15
point_value1 = 100
atr_market2 = 0.0050
point_value2 = 100000 # 假设是外汇
account_equity = 1_000_000
kelly_k = 0.2 # 全局凯利分数
# 1. 分别计算每个策略的凯利 f
f1 = calculate_kelly_basic(trades_pnl_strategy1, kelly_fraction=kelly_k)
f2 = calculate_kelly_basic(trades_pnl_strategy2, kelly_fraction=kelly_k)
print(f"\n策略1 凯利 f1: {f1:.4f}")
print(f"策略2 凯利 f2: {f2:.4f}")
# 2. 分配总风险预算 (简化:假设等权重分配总凯利预算)
# 注意:更合理的方式是直接用 f1, f2 作为各自的风险预算比例
# 这里演示另一种思路:先算一个组合的平均或总凯利预算,再分配
# total_portfolio_f = (f1 + f2) / 2 # 极其简化的假设,忽略相关性等
# 更常见的做法: 直接将 f1, f2 视为各自的风险预算比例,但要确保总风险可控
# 例如,可以对 f1, f2 再做调整,使得总风险不超过某个上限
# 假设我们直接使用计算出的 f1, f2 (如果它们>0)
if f1 > 0:
print("\n--- 计算策略1头寸 ---")
# 假设策略1使用全部权益进行计算(实践中可能有更复杂的资本分配)
pos1 = calculate_position_size_kelly_vol(account_equity, f1, atr_market1, point_value1)
print(f"建议策略1头寸: {pos1}")
else:
print("\n策略1不建议开仓")
if f2 > 0:
print("\n--- 计算策略2头寸 ---")
pos2 = calculate_position_size_kelly_vol(account_equity, f2, atr_market2, point_value2)
print(f"建议策略2头寸: {pos2}")
else:
print("\n策略2不建议开仓")
# 重要提示:这种独立计算未考虑组合效应和相关性!
# 实际组合管理需要更复杂的模型,如均值-方差优化、风险平价等。
# 并且,需要监控组合的总风险敞口,确保 (pos1 * risk_per_contract1 + pos2 * risk_per_contract2) / account_equity
# 不超过预设的总风险上限。可能需要按比例缩减 pos1 和 pos2。
说明:
- 独立计算: 分别为每个策略/市场计算凯利比例
f
。 - 独立应用: 将计算出的
f
(假设为正)和对应市场的波动率信息(ATR, Point Value)代入场景三的函数,计算各自的头寸。 - 简化与风险: 这是极度简化的方法。它忽略了策略/市场间的相关性。如果两个策略高度正相关,这样做可能会导致总风险远超预期。实际应用中,必须考虑组合层面的风险控制,可能需要按比例缩减所有头寸以满足整体风险约束。
总结与注意事项:
- 分数凯利是必须的 (
k < 1
): 不要使用完整凯利。k
的选择依赖于风险偏好、参数估计的信心和回撤承受能力,通常从很小的值开始(如0.1-0.3)。 - 参数估计是关键:
p
和b
的准确性直接影响结果。使用滚动窗口、对参数进行保守调整(打折)是常见的做法。 - 波动率结合: 将凯利(风险预算%)与波动率(计算具体头寸)结合是CTA中非常实用的风险管理技术。
- 组合复杂性: 多策略/多市场的凯利应用需要考虑相关性,通常需要更高级的投资组合优化技术。
- 不仅仅是凯利: 头寸管理只是风险管理的一部分。止损策略、整体组合风险监控、流动性管理等同样重要。
- 回测验证: 任何凯利应用方法都需要在历史数据上进行严格的回测,特别关注最大回撤、夏普比率等指标,并在样本外数据上进行验证。
这些例子提供了一个从基础到进阶应用凯利公式的框架。在实际开发中,你需要根据策略的具体特点、数据的质量和自身的风险管理要求来选择和调整最适合的方法。