问题描述
解决方案
在交易策略中加入凯利公式(Kelly Criterion)的核心目的是根据策略的历史胜率和盈亏比,动态计算最优的仓位比例,从而在控制风险的前提下最大化长期复利增长。
凯利公式简介
在股票交易中,凯利公式的通用形式如下:
$$ f^* = \frac{p(b+1) - 1}{b} = \frac{bp - q}{b} $$
其中:
- $f^*$:最优仓位比例(即本次交易应投入总资金的百分比)。
- $p$:胜率(获利交易次数 / 总交易次数)。
- $q$:败率($1 - p$)。
- $b$:赔率(盈亏比 = 平均盈利金额 / 平均亏损金额)。
策略实现思路
- 数据记录:需要一个机制来记录每一笔平仓交易的盈亏情况。
- 参数计算:根据历史记录动态计算胜率 ($p$) 和盈亏比 ($b$)。
- 仓位控制:在开仓信号触发时,调用凯利公式计算仓位 $f$,并结合“半凯利”(Half-Kelly)等风控手段防止仓位过重。
完整的 JoinQuant 策略代码
以下是一个完整的双均线策略示例,其中集成了凯利公式进行仓位管理。代码会自动记录平仓后的盈亏,并据此调整下一次开仓的比例。
# -*- coding: utf-8 -*-
import numpy as np
import pandas as pd
def initialize(context):
"""
初始化函数
"""
# 设定基准
set_benchmark('000300.XSHG')
# 开启动态复权模式(真实价格)
set_option('use_real_price', True)
# 过滤掉order系列API产生的比error级别低的log
log.set_level('order', 'error')
# --- 策略参数设置 ---
g.security = '000001.XSHE' # 操作标的:平安银行
g.short_window = 5 # 短周期均线
g.long_window = 20 # 长周期均线
# --- 凯利公式相关全局变量 ---
# 记录历史交易盈亏比例的列表 (例如: [0.05, -0.02, 0.10, ...])
g.trade_history = []
# 凯利公式激进程度系数 (Fractional Kelly)
# 1.0 为全凯利(风险极大),通常建议使用 0.5 (半凯利) 或更低来平滑波动
g.kelly_fraction = 0.5
# 初始预设参数 (在没有足够交易记录前使用)
g.default_win_rate = 0.5 # 预设胜率 50%
g.default_odds = 2.0 # 预设盈亏比 2:1
def handle_data(context, data):
"""
按分钟或按天运行的策略逻辑
"""
security = g.security
# 获取历史数据
# 多取一天以防计算均线数据不足
hist_data = attribute_history(security, g.long_window + 1, '1d', ['close'])
close_prices = hist_data['close']
if len(close_prices) < g.long_window:
return
# 计算均线
ma_short = close_prices[-g.short_window:].mean()
ma_long = close_prices[-g.long_window:].mean()
# 获取当前持仓情况
position = context.portfolio.positions[security]
curr_price = close_prices[-1]
# --- 交易信号逻辑 ---
# 1. 卖出信号:短均线下穿长均线 (死叉) 且 有持仓
if ma_short < ma_long and position.closeable_amount > 0:
# 【关键步骤】在卖出前记录这笔交易的盈亏比例
# 盈亏比例 = (当前价格 - 持仓成本) / 持仓成本
if position.avg_cost > 0:
pnl_ratio = (curr_price - position.avg_cost) / position.avg_cost
g.trade_history.append(pnl_ratio)
log.info("平仓录入历史: 盈亏比率 %.2f%%" % (pnl_ratio * 100))
# 执行清仓
order_target(security, 0)
log.info("卖出 %s" % security)
# 2. 买入信号:短均线上穿长均线 (金叉) 且 无持仓
elif ma_short > ma_long and position.total_amount == 0:
# 【关键步骤】使用凯利公式计算仓位
kelly_pos = calculate_kelly_position(g.trade_history)
# 应用“分数凯利”进行风控 (例如只用凯利建议仓位的 50%)
final_pos_ratio = kelly_pos * g.kelly_fraction
# 边界检查:仓位不能小于0,最大不超过100% (1.0)
final_pos_ratio = max(0.0, min(1.0, final_pos_ratio))
# 计算目标买入金额
total_value = context.portfolio.total_value
target_value = total_value * final_pos_ratio
if target_value > 0:
log.info("凯利公式计算仓位: %.2f%%, 实际开仓金额: %.2f" % (final_pos_ratio * 100, target_value))
order_target_value(security, target_value)
def calculate_kelly_position(trade_history):
"""
根据历史交易记录计算凯利公式建议的仓位比例
f = (p * (b + 1) - 1) / b
"""
# 如果历史记录太少,使用默认保守参数,避免初期波动过大
if len(trade_history) < 5:
p = g.default_win_rate
b = g.default_odds
else:
# 将列表转换为 numpy 数组方便计算
history = np.array(trade_history)
# 1. 计算胜率 (p)
# 盈利次数 / 总次数
wins = history[history > 0]
losses = history[history <= 0]
if len(history) == 0:
return 0
p = len(wins) / float(len(history))
# 2. 计算盈亏比 (b)
# 平均盈利金额 / 平均亏损金额 (取绝对值)
avg_win = np.mean(wins) if len(wins) > 0 else 0
avg_loss = abs(np.mean(losses)) if len(losses) > 0 else 1.0 # 避免除以0
# 如果没有亏损过,盈亏比会无穷大,这里做一个限制
if avg_loss == 0:
b = 10.0 # 给予一个较高的盈亏比上限
else:
b = avg_win / avg_loss
# 3. 应用凯利公式
# f = (p * (b + 1) - 1) / b
# 简化公式: f = p - q / b (其中 q = 1-p)
if b == 0:
return 0
f = (p * (b + 1) - 1) / b
# 打印调试信息
# log.info("凯利参数更新: 胜率(p)=%.2f, 盈亏比(b)=%.2f, 建议仓位(f)=%.2f" % (p, b, f))
# 如果计算结果小于0,说明期望收益为负,不应开仓
if f < 0:
return 0
return f
代码关键点解析
-
g.trade_history:- 这是一个全局列表,用于存储每一次平仓后的收益率。
- 在
handle_data的卖出逻辑中,我们计算(curr_price - position.avg_cost) / position.avg_cost并存入列表。
-
calculate_kelly_position函数:- 胜率 ($p$): 统计历史中收益率大于0的次数占比。
- 盈亏比 ($b$): 统计历史平均盈利幅度除以平均亏损幅度(绝对值)。
- 冷启动处理: 当交易次数少于5次时,使用预设的
g.default_win_rate和g.default_odds,防止初期样本不足导致仓位计算极端。
-
分数凯利 (Fractional Kelly):
- 代码中引入了
g.kelly_fraction = 0.5。 - 原因: 理论上的凯利公式假设胜率和赔率是已知且固定的,但在股市中这些参数是估算的且不稳定的。全仓凯利(Full Kelly)往往会导致巨大的资产回撤。业界通常使用“半凯利”或“1/4凯利”来平滑风险。
- 代码中引入了
-
边界检查:
- 计算出的 $f$ 可能是负数(如果期望收益为负),此时应返回 0(不开仓)。
- 计算出的 $f$ 可能大于 1(如果胜率极高),此时限制为 1.0(不加杠杆)。
常见问题 Q&A
Q: 为什么我的凯利公式计算出的仓位经常是负数?
A: 凯利公式的前提是数学期望为正。如果你的策略历史胜率低且盈亏比也不高(即 $p(b+1) < 1$),公式会告诉你不应该下注(结果为负)。这说明策略本身需要优化,或者近期市场表现不适合该策略。
Q: 这个策略可以直接用于实盘吗?
A: 不建议直接使用。上述代码仅为逻辑演示。实盘中需要考虑:
- 加仓逻辑:上述代码假设每次开仓都是针对总资金的重新分配,未考虑分批加仓。
- 多只股票:如果是多只股票组合,凯利公式需要升级为矩阵形式或平均分配,上述代码是针对单只标的的。
- 止损影响:凯利公式对最大亏损非常敏感,必须配合严格的止损策略来稳定盈亏比 ($b$)。
Q: 如何在多只股票中使用?
A: 如果是多只股票,最简单的方法是将计算出的 final_pos_ratio 理解为单只股票占总资金的最大比例,或者将总资金平分成N份,每一份内部使用凯利公式决定投入比例。