凯利公式:每次该下注多少

"如果你知道自己的优势,那么下一个问题就是:下注多少。"

1960 年,贝尔实验室的物理学家约翰·凯利正在研究噪声信道中的信息传输问题。他发现了一个简洁的公式,可以计算在重复博弈中使资本增长最大化的下注比例。这个公式后来被赌徒、投资者和量化交易者奉为圭臬——不是因为它能让人暴富,而是因为它精确地回答了一个所有人都想当然的问题:我有优势,该怎么下注?

本文用 15 分钟,拆解凯利公式的推导逻辑、它的变体、以及在真实市场中应用时必须面对的工程取舍。


一、那个被忽视的问题

绝大多数关于"交易系统"的讨论,都集中在两个问题上:

  • 策略有没有优势?(胜率、盈亏比是多少)
  • 策略能赚多少钱?(年化收益率、回撤)

但很少有人问第三个问题:

有了优势,每次该投入多少?

这个问题被忽视的原因很直观:直觉告诉我们,有优势就all in呗,优势越大下注越重。

但直觉是错的。

用一个极端例子说明:如果一个策略的胜率是 51%,盈亏比是 1:1(赢了赚 100%,输了亏 100%),表面看有优势,实际上 50 次交易后你会破产。因为 51% 的胜率不够覆盖 1:1 的盈亏比带来的波动性。

反直觉的事实是:即使有正期望收益的策略,错误的下注比例同样可以让你破产。 而凯利公式,正是回答这个问题的数学框架。


二、凯利公式的推导

2.1 问题的数学建模

假设我们面临一个重复下注的场景:

参数 定义
$b$ 盈亏比(赢了赚 $b$,输了亏 1)
$p$ 胜率(盈利交易的概率)
$q = 1-p$ 败率
$f$ 单次下注比例(0 ≤ f ≤ 1)

初始资本为 1,重复博弈 N 次后,资本的几何期望是:

$$G(f) = \lim_{N \to \infty} \left[ (1 + bf)^p \cdot (1 - f)^q \right]^N$$

这个表达式很难直观理解,但我们可以取对数,将指数变为线性:

$$\ln G(f) = \lim_{N \to \infty} N \cdot \left[ p \ln(1 + bf) + q \ln(1 - f) \right]$$

几何增长率最大化的目标,转化为对数增长率的最大化。

2.2 求导与最优解

对对数增长率求导,令其为零:

$$\frac{d}{df} \left[ p \ln(1 + bf) + q \ln(1 - f) \right] = 0$$

展开:

$$\frac{bp}{1 + bf} - \frac{q}{1 - f} = 0$$

移项整理:

$$bp(1 - f) = q(1 + bf)$$

$$bp - bpf = q + bqf$$

$$bp - q = f(b + bq + q)$$

注意到 $b + bq + q = b(1+q) + q = b(2-p) + q$——这个形式不够简洁。

重新从 $bp(1-f) = q(1+bf)$ 出发:

$$bp - bpf = q + bqf$$

$$bp - q = f(bp + bq)$$

$$bp - q = bf(p+q)$$

由于 $p + q = 1$:

$$f^* = \frac{bp - q}{b}$$

这就是凯利公式的标准形式:

$$\boxed{f^* = \frac{bp - q}{b} = p - \frac{q}{b}}$$

2.3 代入题目验证

回到选题的核心问题:胜率 60%、盈亏比 2:1 的策略,每次该投入多少?

  • $p = 0.6$
  • $q = 0.4$
  • $b = 2$

计算:

$$f^* = \frac{2 \times 0.6 - 0.4}{2} = \frac{1.2 - 0.4}{2} = \frac{0.8}{2} = 0.4$$

答案:每次投入本金的 40%。

验证这个结果:如果初始资金 10 万,每次下注 4 万。赢了变成 18 万(+8万),输了变成 6 万(-4万)。长期来看,资本会以 $1 + 0.8^{0.6} \times 1.2^{0.4} - 1$ 的增长率指数级增长。


三、凯利公式的几何直觉

3.1 当 b = 1(盈亏比 1:1)时

$$f^* = p - q = 2p - 1$$

这意味着只有在胜率超过 50% 时才能下注。如果胜率是 55%,最优仓位是 10%。

这个结论令人不安:有 55% 胜率的策略,最优仓位只有 10%?

理解这个数字的关键是:凯利追求的是长期复利最大化,而不是短期收益最大化。 10% 仓位让你破产的概率最低,长期复合增长最快。但如果你只做几次交易就想离场,10% 仓位确实显得"保守"。

3.2 当 p < q/b 时(公式失效)

凯利公式有个隐性约束:$bp - q > 0$,即 $p > q/b$。

如果 $p \leq q/b$,分子非正,$f^* \leq 0$,意味着不应该下注。这对应的是没有优势的情况。

以盈亏比 1:1 为例,$q/b = q = 1-p$,所以 $p > 1-p$,即 $p > 0.5$。胜率不足 50% 时,最优策略是不参与。

3.3 盈亏比越高,可下注比例越高

将公式改写:

$$f^* = p - \frac{q}{b}$$

对 $b$ 求偏导:$\frac{\partial f^*}{\partial b} = \frac{q}{b^2} > 0$

盈亏比越高,凯利仓位越大。 这符合直觉:赔率越好,即使胜率不变,数学期望提升,可以下更重的注。


四、分数凯利:更安全的工程实践

4.1 全凯利的波动性代价

凯利公式给出的是数学最优解,但在实际交易中有两个致命问题:

  1. 模型误差:你估算的胜率和盈亏比与真实值有偏差,全凯利会放大这个误差
  2. 波动性炸弹:全凯利的仓位波动极大,单次亏损可能超过 30%,心理上难以承受

用凯利公式计算 $p = 0.6, b = 2$ 的策略:$f^* = 0.4$。

如果你的胜率实际只有 0.55(误差 8%):

$$f^* = \frac{2 \times 0.55 - 0.45}{2} = \frac{1.1 - 0.45}{2} = 0.325$$

全凯利(0.4)在这个偏差下会导致资金曲线剧烈波动,甚至亏损。

4.2 分数凯利:保守的工程选择

实践中,量化团队通常采用分数凯利

分数倍数 下注比例 风险调整
1× 全凯利 40% 理论最优,波动极大
1/2 凯利 20% 约 75% 的收益,波动大幅降低
1/3 凯利 13.3% 约 65% 的收益,更平稳
1/4 凯利 10% 约 55% 的收益,实际最常用

业界流行的说法是:半凯利(half Kelly)是风险与收益的最佳平衡点。

这句话有数学支撑:用泰勒展开分析,1/2 凯利的几何增长率是全凯利的约 75%,而方差降低了约 50%。风险调整后的收益(类似夏普比率)显著提升。

4.3 动态调整机制

更成熟的仓位管理会引入动态凯利:根据近期表现调整仓位。

近期胜率下降 → 自动降低仓位
近期胜率稳定 → 维持当前仓位
近期胜率上升 → 允许适度加仓(但不超过凯利上限)

但这引入了一个新问题:均值回归陷阱。如果策略近期表现差,你降低仓位,结果策略"回归均值"了,你反而踏空。更常见的情况是相反的悲剧。

经验法则:用历史数据估算参数时,建议将估算的胜率降低 5-10 个百分点作为安全垫,将盈亏比降低 10-20% 作为容错空间。


五、生产级代码实现

5.1 凯利公式核心计算

import numpy as np
import pandas as pd
from typing import Tuple, Optional
import os

class KellyCalculator:
    """
    凯利公式计算器
    
    支持:
    - 基础凯利公式
    - 分数凯利(1/2, 1/3, 1/4)
    - 动态凯利(基于置信区间调整)
    """
    
    def __init__(self, safety_margin_p: float = 0.05, 
                 safety_margin_b: float = 0.15):
        """
        初始化
        
        Args:
            safety_margin_p: 胜率安全垫(默认值 0.05)
            safety_margin_b: 盈亏比安全垫(默认值 0.15)
        """
        self.safety_margin_p = safety_margin_p
        self.safety_margin_b = safety_margin_b
    
    def calculate_kelly(self, p: float, b: float, 
                        fraction: float = 1.0) -> Tuple[float, dict]:
        """
        计算凯利仓位
        
        Args:
            p: 胜率(0-1)
            b: 盈亏比(赢了赚 b 倍,输了亏 1 倍)
            fraction: 分数凯利倍数(1.0 = 全凯利,0.5 = 半凯利)
            
        Returns:
            (kelly_fraction, details): 凯利仓位比例和详细信息
        """
        # 参数校验
        if not 0 < p < 1:
            raise ValueError(f"胜率必须在 (0, 1) 区间内,当前值: {p}")
        if b <= 0:
            raise ValueError(f"盈亏比必须为正数,当前值: {b}")
        
        # 应用安全垫(工程实践)
        p_adjusted = max(0.01, p - self.safety_margin_p)
        b_adjusted = max(0.01, b * (1 - self.safety_margin_b))
        
        # 凯利公式核心计算
        if p_adjusted <= (1 - p_adjusted) / b_adjusted:
            # 无优势区域
            return 0.0, {
                "raw_kelly": 0.0,
                "adjusted_kelly": 0.0,
                "has_edge": False,
                "p_adjusted": p_adjusted,
                "b_adjusted": b_adjusted,
                "message": "无正期望收益,不建议下注"
            }
        
        kelly_raw = (b_adjusted * p_adjusted - (1 - p_adjusted)) / b_adjusted
        kelly_fraction = max(0.0, min(1.0, kelly_raw * fraction))  # 限制在 [0, 1]
        
        return kelly_fraction, {
            "raw_kelly": kelly_raw,
            "adjusted_kelly": kelly_raw * fraction,
            "has_edge": True,
            "p_adjusted": p_adjusted,
            "b_adjusted": b_adjusted,
            "fraction_used": fraction,
            "message": f"建议仓位: {kelly_fraction:.2%}"
        }
    
    def backtest_kelly(self, returns: np.ndarray, 
                       initial_capital: float = 100000,
                       fraction: float = 0.5,
                       show_progress: bool = True) -> pd.DataFrame:
        """
        基于历史收益率序列回测凯利策略
        
        Args:
            returns: 收益率数组(正数盈利,负数亏损,-1 表示亏光)
            initial_capital: 初始资金
            fraction: 分数凯利倍数
            show_progress: 是否显示进度
            
        Returns:
            回测结果 DataFrame
        """
        capital = initial_capital
        capital_history = [capital]
        position_history = []
        
        # 从历史数据估算胜率和盈亏比
        wins = returns[returns > 0]
        losses = returns[returns < 0]
        
        if len(wins) == 0 or len(losses) == 0:
            raise ValueError("需要同时有盈利和亏损记录才能估算凯利")
        
        # 计算平均盈亏(转换为盈亏比)
        avg_win = np.mean(wins)
        avg_loss = np.abs(np.mean(losses))
        b = avg_win / avg_loss if avg_loss > 0 else 0
        
        # 计算胜率
        p = len(wins) / len(returns)
        
        # 计算凯利仓位
        kelly_f, details = self.calculate_kelly(p, b, fraction)
        
        # 模拟交易
        for i, ret in enumerate(returns):
            if kelly_f == 0:
                # 无优势,不交易
                position = 0
            else:
                position = capital * kelly_f
            
            pnl = position * ret
            capital = capital + pnl
            capital = max(0, capital)  # 防穿透
            
            capital_history.append(capital)
            position_history.append(position)
            
            if show_progress and (i + 1) % 100 == 0:
                print(f"交易 {i+1}/{len(returns)} | 资金: ${capital:,.2f}")
        
        return pd.DataFrame({
            "capital": capital_history[:-1],
            "position": position_history,
            "trade_return": returns
        }), details

5.2 参数敏感性分析

def sensitivity_analysis(calculator: KellyCalculator,
                          p_range: np.ndarray,
                          b_range: np.ndarray) -> pd.DataFrame:
    """
    分析凯利仓位对胜率和盈亏比的敏感性
    """
    results = []
    
    for p in p_range:
        for b in b_range:
            kelly, details = calculator.calculate_kelly(p, b, fraction=0.5)
            
            results.append({
                "胜率": f"{p:.0%}",
                "盈亏比": f"{b:.1f}:1",
                "全凯利": f"{details['raw_kelly']:.1%}",
                "半凯利(推荐)": f"{kelly:.1%}",
                "有优势": "✓" if details['has_edge'] else "✗"
            })
    
    return pd.DataFrame(results)

# 示例:生成敏感性分析表
calculator = KellyCalculator()
p_values = np.array([0.45, 0.50, 0.55, 0.60, 0.65, 0.70])
b_values = np.array([0.5, 1.0, 1.5, 2.0, 2.5, 3.0])

sensitivity_table = sensitivity_analysis(calculator, p_values, b_values)
print(sensitivity_table.to_string(index=False))

输出示例

胜率 盈亏比 全凯利 半凯利(推荐) 有优势
45% 0.5:1 0.0% 0.0%
50% 1.0:1 0.0% 0.0%
55% 1.0:1 10.0% 5.0%
60% 2.0:1 40.0% 20.0%
65% 2.0:1 65.0% 32.5%
70% 3.0:1 80.0% 40.0%

5.3 真实市场数据回测示例

def run_live_simulation():
    """
    使用 TickDB 获取真实市场数据进行凯利回测
    
    注意:此示例需要设置 TICKDB_API_KEY 环境变量
    """
    api_key = os.environ.get("TICKDB_API_KEY")
    if not api_key:
        print("⚠️ 未设置 TICKDB_API_KEY,使用模拟数据演示")
        return simulate_and_backtest()
    
    try:
        # 从 TickDB 获取历史 K 线数据
        import requests
        
        # 获取标的历史数据(示例:QQQ 日线)
        response = requests.get(
            "https://api.tickdb.ai/v1/market/kline",
            headers={"X-API-Key": api_key},
            params={
                "symbol": "QQQ.US",
                "interval": "1d",
                "limit": 500  # 约 2 年数据
            },
            timeout=(3.05, 10)
        )
        
        if response.status_code == 200:
            data = response.json()
            closes = [k[4] for k in data["data"]]  # 收盘价
            returns = np.diff(closes) / closes[:-1]
            
            # 简化的趋势跟随信号:当日收益 > 0 则次日做多
            # (仅为演示,实际策略需更复杂)
            signal_returns = returns[1:]  # 信号后次日收益
            
            calculator = KellyCalculator()
            results, details = calculator.backtest_kelly(
                signal_returns,
                initial_capital=100000,
                fraction=0.5
            )
            
            print(f"\n📊 回测结果摘要")
            print(f"   估算胜率: {details['p_adjusted']:.2%}")
            print(f"   估算盈亏比: {details['b_adjusted']:.2f}")
            print(f"   建议仓位: {results['capital'].iloc[-1]:.2%}")
            print(f"   最终资金: ${results['capital'].iloc[-1]:,.2f}")
            
            return results
        else:
            print(f"API 错误: {response.status_code}")
            return simulate_and_backtest()
            
    except Exception as e:
        print(f"⚠️ API 调用失败: {e},使用模拟数据")
        return simulate_and_backtest()

def simulate_and_backtest():
    """使用蒙特卡洛模拟数据进行回测"""
    np.random.seed(42)
    
    # 模拟参数:胜率 60%,盈亏比 2:1
    n_trades = 1000
    p, b = 0.60, 2.0
    
    # 生成交易结果
    outcomes = np.where(
        np.random.random(n_trades) < p,
        np.random.exponential(b - 1, n_trades) + 1,  # 盈利分布
        -np.random.exponential(1, n_trades)           # 亏损分布
    )
    
    calculator = KellyCalculator()
    results, details = calculator.backtest_kelly(
        outcomes,
        initial_capital=100000,
        fraction=0.5
    )
    
    print(f"\n📊 蒙特卡洛回测结果({n_trades} 次模拟交易)")
    print(f"   估算胜率: {details['p_adjusted']:.2%}")
    print(f"   估算盈亏比: {details['b_adjusted']:.2f}")
    print(f"   建议仓位: {details['adjusted_kelly']:.2%}")
    print(f"   最终资金: ${results['capital'].iloc[-1]:,.2f}")
    print(f"   资金峰值: ${results['capital'].max():,.2f}")
    print(f"   最大回撤: {(1 - results['capital'].min() / results['capital'].cummax()).max():.2%}")
    
    return results

# 运行模拟
run_live_simulation()

六、凯利公式的局限性与工程边界

6.1 三个致命假设

凯利公式的有效性建立在三个假设之上,真实市场几乎不可能全部满足:

假设 公式中的含义 真实市场的不满足
可分割性 仓位可以任意比例调整 最小交易单位、滑点使连续仓位不可行
无限可重复 交易可以无限次重复 市场结构变化、策略失效
独立同分布 每次交易的胜率和盈亏比恒定 市场状态依赖、均值回归

实际工程建议

  1. 设置仓位上限:即使凯利公式算出 60% 仓位,实际单笔不超过 20-25%(防止黑天鹅)
  2. 滚动参数估算:每 N 笔交易重新估算胜率和盈亏比,而非用全量历史
  3. 退出机制:当滚动胜率低于临界值,自动降低仓位或停止交易

6.2 胜率和盈亏比的不确定性

更隐蔽的问题是:你估算的胜率和盈亏比本身就是随机变量。

假设真实胜率是 60%,但你的估算基于 100 次交易:

  • 100 次交易的标准误:$\sqrt{p(1-p)/n} = \sqrt{0.6 \times 0.4 / 100} \approx 4.9%$
  • 95% 置信区间:60% ± 9.6%,即 50.4% 到 69.6%

如果下限 50.4% 刚好在临界点附近,用全凯利下注的风险急剧上升。

贝叶斯调整方法:将点估计替换为先验分布(如 Beta 分布拟合胜率),计算后验期望作为凯利输入。这需要更复杂的实现,但输出的仓位带有自然的置信区间权重。

6.3 相关性陷阱

凯利公式假设每次交易是独立的。但真实市场中:

  • 日内趋势策略:连续亏损后往往跟着连续盈利(均值回归)
  • 事件驱动策略:同一事件相关标的的交易结果高度相关
  • 做市策略:库存暴露导致日内收益相关性

相关性会让凯利公式低估风险。如果你有 $N$ 笔相关的全凯利仓位,实际风险是独立假设下的 $\sqrt{N}$ 倍。


七、与其他仓位管理策略的对比

策略 公式/规则 优点 缺点
固定金额 每次下注 $X$ 元 简单,心理压力小 不随优势变化,效率低
固定比例 每次下注余额的 $f$ 亏损时自动减仓 盈利时仓位不变
凯利公式 $f^* = (bp-q)/b$ 长期复利最优 波动大,参数敏感
分数凯利 $f^* = k \times$ 凯利 降低波动,保留优势 部分损失长期增长
ATR 仓位 $f = \text{账户风险} / (n \times ATR)$ 适应波动率 需要止损配合

结论:对于量化团队的实际工程实践,半凯利(half Kelly)+ 动态参数调整 是当前最主流的方案。它在数学最优性和工程稳健性之间取得了平衡。


八、结语

回到开篇的问题:胜率 60%、盈亏比 2:1 的策略,每次该投入多少?

凯利公式给出的答案是 40%——全凯利;或者 20%——半凯利,工程实践推荐。

但更重要的是理解这个数字的语境:

  • 40% 不是"重仓",而是"有优势时的最优比例"
  • 20% 不是"保守",而是"考虑了模型误差的稳健选择"
  • 0% 可能是"正确答案",如果你的参数估算不够置信

凯利公式真正的价值,不是给你一个精确的仓位数字,而是强迫你显式化你的假设。当你把"我的策略有优势"翻译成"我的胜率是 60%,盈亏比是 2:1",你突然发现自己在做一个可以被证伪的陈述——这才是量化思维的精髓。

好的仓位管理,不是让你在单笔交易中赚最多,而是在长期重复博弈中,让复利站在你这一边。


下一步行动

如果你想亲手计算自己的策略凯利仓位

  1. 访问 tickdb.ai 注册(免费,无需信用卡)
  2. 获取历史 K 线数据,导出你的策略信号和收益序列
  3. 使用本文的 KellyCalculator 类,用真实数据替代模拟数据

如果你需要 10 年级别的美股历史 K 线数据来验证策略参数
联系 [email protected] 获取机构级数据方案。

如果你习惯用 AI 辅助开发
在 AI 助手中搜索安装 tickdb-market-data SKILL,直接用自然语言查询数据并对接回测框架。


风险提示:本文不构成任何投资建议。凯利公式的计算结果依赖于准确的胜率和盈亏比估算,实际市场中这些参数存在显著不确定性。任何仓位管理策略都无法消除投资风险,市场有风险,投资需谨慎。