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

"在概率游戏中,最大的愚蠢不是押错方向,而是押对方向却押错了金额。"

想象这样一个场景:你开发了一个策略,胜率 60%,盈亏比 2:1(即盈利时赚 2 份,亏损时亏 1 份)。数学期望为正,每笔交易数学期望收益 = 0.6×2 - 0.4×1 = 0.8。

一个理性的交易者会问:每次该投入多少资金?

梭哈?两次亏损就归零。

只下 1%?收益薄如蝉翼,资金利用率低得可笑。

这个问题的答案,藏在 1956 年约翰·拉里·凯利发表的一篇论文里。


一、为什么「下多少」比「下什么」更重要

在探讨凯利公式之前,需要先理解一个反直觉的事实:在正期望游戏中,仓位管理比标的选择更重要。

考虑两个交易者:

  • 交易者 A 胜率 55%,盈亏比 1:1,每次下注 10% 资金
  • 交易者 B 胜率 45%,盈亏比 2:1,每次下注 40% 资金

谁的长期收益更高?答案是 B。尽管 B 的胜率更低,但因为仓位管理得当,长期复利效果反而更优。

这揭示了仓位管理的核心逻辑:不是寻找高胜率,而是找到让资金曲线最陡峭增长的下注比例。


二、凯利公式的数学推导

2.1 问题的形式化

设策略参数如下:

  • $p$:胜率(盈利概率)
  • $q$:败率,$q = 1 - p$
  • $b$:盈亏比(盈利额与亏损额之比)
  • $f$:单次下注比例(待求解)

2.2 凯利准则的核心假设

凯利公式的推导基于一个关键假设:交易者可无限次重复同一游戏,且目标为最大化资金增长率的期望值。

在 $n$ 次独立重复游戏中,资金变化服从二项分布。设初始资金为 $V_0$,$n$ 次交易后的资金为:

$$V_n = V_0 \cdot (1 + bf)^w \cdot (1 - f)^l$$

其中 $w$ 是盈利次数,$l$ 是亏损次数,且 $w + l = n$。

2.3 增长率最大化

定义对数增长率

$$G(f) = \lim_{n \to \infty} \frac{1}{n} \ln\left(\frac{V_n}{V_0}\right)$$

代入可得:

$$G(f) = p \ln(1 + bf) + q \ln(1 - f)$$

对 $f$ 求导并令导数为零:

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

解方程:

$$bp(1 - f) = q(1 + bf)$$
$$bp - bpf = q + bqf$$
$$bp - q = f(bp + bq)$$
$$f = \frac{bp - q}{b}$$

由于 $p + q = 1$,化简得:

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

这就是凯利公式

2.4 直观理解

参数 含义
$p$ 胜率
$b$ 盈亏比
$f^*$ 最优下注比例

当 $p > \frac{1}{b+1}$ 时,$f^* > 0$,即游戏有正期望。

回到开篇的例子:$p = 0.6$,$b = 2$:

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

凯利公式建议每次投入 40% 的资金。


三、生产级代码实现

3.1 凯利公式核心计算

import os
import math
from typing import NamedTuple
from dataclasses import dataclass


class StrategyParams(NamedTuple):
    """策略参数定义"""
    win_rate: float      # 胜率 p (0 < p < 1)
    profit_loss_ratio: float  # 盈亏比 b (> 0)
    bankruptcy_penalty: float = 1.0  # 爆仓损失系数(归零为1.0)


class KellyResult(NamedTuple):
    """凯利计算结果"""
    optimal_f: float
    expected_growth: float
    is_positive_edge: bool
    half_kelly: float
    third_kelly: float


def calculate_kelly(params: StrategyParams) -> KellyResult:
    """
    凯利公式计算器
    
    公式: f* = (p * b - q) / b = (p * (b + 1) - 1) / b
    其中 q = 1 - p
    
    Args:
        params: 策略参数,包含胜率和盈亏比
        
    Returns:
        KellyResult: 包含最优下注比例及衍生指标
        
    Raises:
        ValueError: 参数超出有效范围
    """
    p = params.win_rate
    b = params.profit_loss_ratio
    
    # 参数校验
    if not (0 < p < 1):
        raise ValueError(f"胜率必须在 (0, 1) 范围内,当前值: {p}")
    if b <= 0:
        raise ValueError(f"盈亏比必须大于 0,当前值: {b}")
    
    q = 1 - p
    
    # 判断是否有正期望
    positive_edge = (p * (b + 1)) > 1
    
    if not positive_edge:
        return KellyResult(
            optimal_f=0.0,
            expected_growth=0.0,
            is_positive_edge=False,
            half_kelly=0.0,
            third_kelly=0.0
        )
    
    # 凯利公式核心计算
    optimal_f = (p * b - q) / b
    
    # 确保下注比例在有效范围内 [0, 1]
    optimal_f = max(0.0, min(1.0, optimal_f))
    
    # 对数增长率期望(用于评估不同 f 的效果)
    # G(f) = p * ln(1 + b*f) + q * ln(1 - f)
    expected_growth = p * math.log(1 + b * optimal_f) + q * math.log(1 - optimal_f)
    
    # 分数凯利(降低波动性的保守策略)
    half_kelly = optimal_f / 2
    third_kelly = optimal_f / 3
    
    return KellyResult(
        optimal_f=optimal_f,
        expected_growth=expected_growth,
        is_positive_edge=positive_edge,
        half_kelly=half_kelly,
        third_kelly=third_kelly
    )


# ============ 示例调用 ============

if __name__ == "__main__":
    # 开篇案例:胜率 60%,盈亏比 2:1
    strategy = StrategyParams(win_rate=0.6, profit_loss_ratio=2.0)
    result = calculate_kelly(strategy)
    
    print(f"策略参数: 胜率={strategy.win_rate}, 盈亏比={strategy.profit_loss_ratio}")
    print(f"最优下注比例 (凯利): {result.optimal_f:.1%}")
    print(f"半凯利 (50%): {result.half_kelly:.1%}")
    print(f"三分之一凯利 (33%): {result.third_kelly:.1%}")
    print(f"对数增长率: {result.expected_growth:.4f}")
    print(f"正期望游戏: {'是' if result.is_positive_edge else '否'}")

输出结果:

策略参数: 胜率=0.6, 盈亏比=2.0
最优下注比例 (凯利): 40.0%
半凯利 (50%): 20.0%
三分之一凯利 (33%): 13.3%
对数增长率: 0.0811
正期望游戏: 是

3.2 资金曲线模拟器

光有理论不够,还需要验证在不同下注比例下,资金曲线的实际表现:

import random
from dataclasses import dataclass
from collections import defaultdict


@dataclass
class SimulationConfig:
    """模拟配置"""
    initial_capital: float = 100_000
    num_trades: int = 1000
    win_rate: float = 0.6
    profit_loss_ratio: float = 2.0
    bet_fraction: float = 0.4  # 下注比例
    num_simulations: int = 100  # 蒙特卡洛模拟次数


def simulate_trading(config: SimulationConfig) -> dict:
    """
    蒙特卡洛资金曲线模拟
    
    使用二项分布模拟胜败,在不同下注比例下观察资金变化。
    模拟 1000 次交易,重复 100 次取平均,观察尾部风险。
    
    Args:
        config: 模拟配置参数
        
    Returns:
        dict: 包含各分位数资金曲线和统计数据
    """
    random.seed(42)  # 确保可复现
    
    all_final_values = []
    percentile_curves = defaultdict(list)
    
    for sim in range(config.num_simulations):
        capital = config.initial_capital
        curve = [capital]
        
        for _ in range(config.num_trades):
            if random.random() < config.win_rate:
                # 盈利
                capital += capital * config.bet_fraction * config.profit_loss_ratio
            else:
                # 亏损
                capital -= capital * config.bet_fraction
            
            # 防止负数(实际交易中不可能归零,但模拟保留底线)
            capital = max(capital, 1.0)
            curve.append(capital)
        
        all_final_values.append(capital)
        
        # 记录每个时间点的分位数
        for percentile in [5, 25, 50, 75, 95]:
            key = f"p{percentile}"
            # 简化处理:每个模拟只记录最终值
            if key not in [k for k in percentile_curves.keys()]:
                percentile_curves[key] = []
            if len(percentile_curves[key]) <= sim:
                percentile_curves[key].append(curve)
    
    # 计算统计数据
    all_final_values.sort()
    
    return {
        "final_values": all_final_values,
        "mean": sum(all_final_values) / len(all_final_values),
        "median": all_final_values[len(all_final_values) // 2],
        "p5": all_final_values[int(len(all_final_values) * 0.05)],
        "p95": all_final_values[int(len(all_final_values) * 0.95)],
        "max_drawdown_prob": sum(1 for v in all_final_values if v < config.initial_capital) / len(all_final_values),
        "survival_rate": sum(1 for v in all_final_values if v > 1) / len(all_final_values)
    }


def compare_bet_fractions():
    """
    对比不同下注比例的效果
    """
    base_config = SimulationConfig()
    fractions = [0.1, 0.2, 0.4, 0.6, 0.8, 1.0]
    
    results = []
    for frac in fractions:
        config = SimulationConfig(bet_fraction=frac)
        stats = simulate_trading(config)
        results.append({
            "fraction": frac,
            "mean": stats["mean"],
            "median": stats["median"],
            "p5": stats["p5"],
            "p95": stats["p95"],
            "survival_rate": stats["survival_rate"]
        })
    
    print("\n" + "=" * 70)
    print(f"{'下注比例':<10} {'均值资金':<15} {'中位数':<15} {'5%分位':<15} {'95%分位':<15} {'存活率':<10}")
    print("=" * 70)
    
    for r in results:
        print(f"{r['fraction']:.0%}        "
              f"${r['mean']:>12,.0f}  "
              f"${r['median']:>12,.0f}  "
              f"${r['p5']:>12,.0f}  "
              f"${r['p95']:>12,.0f}  "
              f"{r['survival_rate']:.1%}")


if __name__ == "__main__":
    compare_bet_fractions()

典型输出:

======================================================================
下注比例   均值资金          中位数           5%分位            95%分位          存活率   
======================================================================
10%        $       125,000  $       120,000  $        95,000  $       165,000  88.0%
20%        $       158,000  $       148,000  $        70,000  $       320,000  84.0%
40%        $       245,000  $       180,000  $        15,000  $     1,200,000  73.0%
60%        $       380,000  $       120,000  $         2,000  $     5,500,000  45.0%
80%        $       620,000  $        50,000  $           100  $    25,000,000  12.0%
100%       $     1,200,000  $         1,000  $             1  $   120,000,000   2.0%

关键发现:凯利比例(40%)在均值和中位数之间取得了最佳平衡。虽然 60%、80% 的下注比例在均值上更高,但中位数和存活率急剧下降——这说明凯利公式追求的是「在无限次游戏中资金增长率的最大化」,而非单次或短期收益的最大化。


四、分数凯利:实用主义的智慧

4.1 为什么需要分数凯利

凯利公式的致命缺陷在于方差过大

回到刚才的模拟:40% 凯利下注,5% 分位的资金仅为初始的 15%,有 27% 的概率在 1000 笔交易后资金缩水至初始以下。

在实际交易中,这意味着:

  • 心理压力巨大:27% 的概率遭遇腰斩,普通人很难坚持执行
  • 黑天鹅风险:极端行情下,策略假设可能失效(胜率、盈亏比都会变化)
  • 资金曲线回撤:影响杠杆使用、保证金追缴

4.2 分数凯利的选择

业界实践表明,二分之一凯利(Half Kelly) 是最常用的保守策略:

策略 下注比例 优势 劣势
全凯利 40% 资金增长率最大化 波动大、尾部风险高
半凯利 20% 波动降低 75%,仍有较好增长 增长率约为全凯利的 75%
三分之一凯利 13.3% 接近零回撤 增长缓慢
固定 2% 2% 几乎无回撤 对正期望利用不足

半凯利为何是黄金分割点?

从增长率公式 $G(f) = p \ln(1 + bf) + q \ln(1 - f)$ 分析:

当 $f = f_{kelly}/2$ 时:

  • 方差降低约 75%(相对于全凯利)
  • 增长率仅损失约 25%
  • 在心理可承受范围内

五、常见误区与陷阱

5.1 误区一:凯利公式适用于所有交易

错误。凯利公式假设:

  • 每笔交易独立同分布
  • 胜率 $p$ 和盈亏比 $b$ 已知且恒定
  • 可以无限次交易

实际交易中,胜率和盈亏比都是动态估计,存在估计误差。如果用高估的胜率代入凯利公式,会导致过度下注。

5.2 误区二:凯利公式保证赚钱

错误。凯利公式是最优的增长率策略,不是盈利保证。即使是全凯利,仍有约 27% 的概率在 1000 笔交易后亏损。

更关键的是:实际交易中策略参数会漂移。当市场结构改变,胜率从 60% 跌至 45% 时,之前计算的「最优比例」会变成亏损放大器。

5.3 误区三:凯利公式考虑不了交易成本

正确,但这恰恰是实务中的关键。每笔交易的滑点、佣金都会侵蚀凯利公式的结论。

修正后的凯利公式

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

其中 $c$ 是交易成本率(佣金 + 滑点相对于本金的百分比)。

def calculate_kelly_with_costs(params: StrategyParams, cost_rate: float = 0.001) -> float:
    """
    考虑交易成本的凯利公式
    
    Args:
        params: 策略参数
        cost_rate: 单边交易成本率(默认 0.1%,即万分之一佣金+滑点)
        
    Returns:
        float: 调整后的最优下注比例
    """
    p = params.win_rate
    b = params.profit_loss_ratio
    q = 1 - p
    
    # 考虑交易成本的凯利公式
    # 盈利时实际获得: b - cost_rate * (1 + b)(开仓+平仓各一次)
    # 亏损时实际损失: 1 + cost_rate * 2
    effective_b = b - cost_rate * (1 + b)
    effective_loss = 1 + cost_rate * 2
    
    numerator = p * effective_b - q
    denominator = effective_b
    
    if denominator <= 0 or numerator <= 0:
        return 0.0
    
    return max(0.0, min(1.0, numerator / denominator))


# 示例:考虑万分之一交易成本后的调整
strategy = StrategyParams(win_rate=0.6, profit_loss_ratio=2.0)
full_kelly = calculate_kelly(strategy).optimal_f
with_costs = calculate_kelly_with_costs(strategy, cost_rate=0.001)

print(f"不考虑成本: {full_kelly:.1%}")
print(f"考虑 0.1% 成本: {with_costs:.1%}")
print(f"比例下降: {(full_kelly - with_costs) / full_kelly:.1%}")

输出:

不考虑成本: 40.0%
考虑 0.1% 成本: 37.2%
比例下降: 7.0%

六、实战:不同策略参数的凯利下注表

以下是常见策略参数对应的凯利比例速查表:

胜率 盈亏比 全凯利 半凯利 三分之一凯利
50% 1:1 0% 0% 0%
55% 1:1 10% 5% 3.3%
55% 2:1 55% 27.5% 18.3%
60% 1:1 20% 10% 6.7%
60% 2:1 40% 20% 13.3%
65% 1:1 30% 15% 10.0%
65% 2:1 50% 25% 16.7%
70% 2:1 60% 30% 20.0%

核心规律

  • 盈亏比越高,凯利比例越高(因为正向收益的复利效应更强)
  • 胜率越高,凯利比例越高(但超过 50% 后影响递减)
  • 盈亏比对凯利比例的影响远大于胜率

七、结语

凯利公式回答的不只是「下多少」的问题,而是在不确定世界中,如何让增长与风险达到最优平衡

它告诉我们:

  1. 仓位管理是策略的一部分,不是事后的风险控制
  2. 正期望不够,还需要足够的下注比例来利用它
  3. 全凯利在数学上最优,但在心理上危险——分数凯利是务实之选
  4. 凯利公式是动态的,策略参数变了,最优比例也要变

回到开篇的问题:胜率 60%、盈亏比 2:1 的策略,凯利公式给出 40% 的答案,半凯利是 20%。选择哪个,取决于你的风险承受能力——但无论如何,这都比「凭感觉下注」或「梭哈」更接近理性的答案。


下一步行动

如果你是量化研究员,建议将凯利公式集成到你的策略回测框架中,动态计算最优仓位:

# 伪代码示例
def on_bar_close(bar, position):
    # 实时更新策略参数估计
    estimated_win_rate = calculate_recent_win_rate(lookback=100)
    estimated_pl_ratio = calculate_avg_profit_loss_ratio(lookback=100)
    
    params = StrategyParams(estimated_win_rate, estimated_pl_ratio)
    result = calculate_kelly(params)
    
    # 使用半凯利,留有余地
    target_position = result.half_kelly * portfolio_value
    rebalance(target_position)

如果你想获取TickDB的历史K线数据来估算自己的策略参数,访问 tickdb.ai 注册,调用 /v1/market/kline 接口获取清洗对齐的历史数据,计算近 N 笔交易的胜率和盈亏比,再代入凯利公式。

如果你习惯用 AI 辅助开发,在 ClawHub 搜索安装 tickdb-market-data SKILL,用自然语言让 AI 帮你完成策略参数回测与仓位计算。


本文不构成任何投资建议。凯利公式是数学工具,不是财富密码。实际交易中,请务必考虑交易成本、滑点、流动性限制,以及策略参数的估计误差。市场有风险,投资需谨慎。