🚀 AI 一键生成 joinquant 策略代码

如何在 JoinQuant 策略中应用凯利公式进行动态仓位管理?

joinquant | 发布时间: 2025/12/16
以下内容由 EasyQuant 生成。

问题描述

如何在我的交易策略里加入凯利公式来管理仓位?

解决方案

在交易策略中加入凯利公式(Kelly Criterion)的核心目的是根据策略的历史胜率和盈亏比,动态计算最优的仓位比例,从而在控制风险的前提下最大化长期复利增长。

凯利公式简介

在股票交易中,凯利公式的通用形式如下:

$$ f^* = \frac{p(b+1) - 1}{b} = \frac{bp - q}{b} $$

其中:

  • $f^*$:最优仓位比例(即本次交易应投入总资金的百分比)。
  • $p$:胜率(获利交易次数 / 总交易次数)。
  • $q$:败率($1 - p$)。
  • $b$:赔率(盈亏比 = 平均盈利金额 / 平均亏损金额)。

策略实现思路

  1. 数据记录:需要一个机制来记录每一笔平仓交易的盈亏情况。
  2. 参数计算:根据历史记录动态计算胜率 ($p$) 和盈亏比 ($b$)。
  3. 仓位控制:在开仓信号触发时,调用凯利公式计算仓位 $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

代码关键点解析

  1. g.trade_history:

    • 这是一个全局列表,用于存储每一次平仓后的收益率。
    • handle_data 的卖出逻辑中,我们计算 (curr_price - position.avg_cost) / position.avg_cost 并存入列表。
  2. calculate_kelly_position 函数:

    • 胜率 ($p$): 统计历史中收益率大于0的次数占比。
    • 盈亏比 ($b$): 统计历史平均盈利幅度除以平均亏损幅度(绝对值)。
    • 冷启动处理: 当交易次数少于5次时,使用预设的 g.default_win_rateg.default_odds,防止初期样本不足导致仓位计算极端。
  3. 分数凯利 (Fractional Kelly):

    • 代码中引入了 g.kelly_fraction = 0.5
    • 原因: 理论上的凯利公式假设胜率和赔率是已知且固定的,但在股市中这些参数是估算的且不稳定的。全仓凯利(Full Kelly)往往会导致巨大的资产回撤。业界通常使用“半凯利”或“1/4凯利”来平滑风险。
  4. 边界检查:

    • 计算出的 $f$ 可能是负数(如果期望收益为负),此时应返回 0(不开仓)。
    • 计算出的 $f$ 可能大于 1(如果胜率极高),此时限制为 1.0(不加杠杆)。

常见问题 Q&A

Q: 为什么我的凯利公式计算出的仓位经常是负数?
A: 凯利公式的前提是数学期望为正。如果你的策略历史胜率低且盈亏比也不高(即 $p(b+1) < 1$),公式会告诉你不应该下注(结果为负)。这说明策略本身需要优化,或者近期市场表现不适合该策略。

Q: 这个策略可以直接用于实盘吗?
A: 不建议直接使用。上述代码仅为逻辑演示。实盘中需要考虑:

  1. 加仓逻辑:上述代码假设每次开仓都是针对总资金的重新分配,未考虑分批加仓。
  2. 多只股票:如果是多只股票组合,凯利公式需要升级为矩阵形式或平均分配,上述代码是针对单只标的的。
  3. 止损影响:凯利公式对最大亏损非常敏感,必须配合严格的止损策略来稳定盈亏比 ($b$)。

Q: 如何在多只股票中使用?
A: 如果是多只股票,最简单的方法是将计算出的 final_pos_ratio 理解为单只股票占总资金的最大比例,或者将总资金平分成N份,每一份内部使用凯利公式决定投入比例。