夏普比率的陷阱:为什么 2.0 的策略可能不如 1.0

价格是符号,波动是语言,而你的指标可能一直在说谎。

2024 年,一位量化开发者兴奋地发了一条朋友圈:“策略夏普 2.3,最大回撤 18%,终于找到圣杯了。”三个月后,他在群里问:“有没有人做过最大回撤超过 60% 以后的心理建设?”夏普 2.3 的策略在 2024 年 Q4 的极端波动中连续触发止损,最终以 63% 的最大回撤收尾。

这不是策略的失败,是指标选择认知的失败。

在量化社区,我们太习惯用一把尺子量尽天下策略:夏普比率。高于 1.0 算及格,2.0 算优秀,3.0 算顶级。但这把尺子刻度背后的假设是什么?它测量的到底是什么?它又遗漏了什么?

本文不是要否定夏普比率——它是风险管理史上最重要的发明之一。本文要做的是:拆解它的隐含假设,揭示它忽略的风险维度,并给出可操作的综合评估框架。读完本文,你会对“为什么夏普 2.0 的策略可能不如 1.0”有一个清醒的答案,并知道如何在回测阶段就设计更健壮的策略评估体系。


一、问题出在哪里:夏普比率的三大隐含假设

夏普比率的计算公式是:

夏普比率 = (年化收益率 - 无风险利率) / 年化波动率

这个公式简洁优美,但它有三个隐含假设,就像一座大厦的地基,平时看不见,但一旦地面震动,问题就会暴露。

1.1 假设一:收益服从正态分布

夏普假设资产的日收益率近似服从正态分布。这意味着:极端事件(3 个标准差以外)几乎不会发生。按照正态分布,3 个标准差以外的事件概率约为 0.13%,也就是说每 770 个交易日(约 3 年)才应该出现一次。

但实际市场是“尖峰厚尾”的。

诺贝尔经济学奖得主 Benoit Mandelbrot 在 1963 年的研究中就已发现:金融市场的收益分布远比正态分布有更厚的尾部。“标普 500”日收益率的标准差约为 1%,但历史上跌幅超过 5% 的交易日数量,是正态分布预测的 50 倍以上。

指标 正态分布预测 标普 500 实际(1960-2020)
日收益率标准差 1% 1%
年极端波动次数(>3σ) 0.6 次 20-30 次
最糟糕一天的损失 约 3.3% -20% (1987 年 10 月 19 日)

当收益分布的实际尾部比正态分布厚得多时,“每 770 天出现一次”的极端事件可能每年发生 20 次。这意味着:夏普比率系统性低估了尾部风险——它把“几乎不会发生”的事当成了“确实不会发生”。

1.2 假设二:上行波动与下行波动等价

夏普比率的分母是“年化波动率”,计算时对上行和下行波动一视同仁。

这在逻辑上有一个微妙的问题:如果一个策略能在趋势行情中稳定盈利(高上行波动),在震荡行情中反复被扫止损(高下行波动),它的夏普比率可能相同,但投资者体验天差地别。

举一个具体的例子:

阶段 日均收益 收益率标准差 说明
趋势行情(40% 时间) +0.8% 0.3% 策略稳定盈利
震荡行情(60% 时间) -0.15% 0.8% 频繁假突破,反复止损

如果合并计算:年化收益率 = 40% × 0.8% × 250 + 60% × (-0.15%) × 250 ≈ 12.25%;年化波动率 ≈ 22%。夏普 ≈ 0.55。

但如果你只看趋势行情期间的收益特征:夏普高达 2.67。问题是:这 2.67 能代表整体策略吗?

这个问题的答案是:不能。上行波动是策略的“奖励”,下行波动是策略的“代价”。把它们放进同一个分母,就相当于把“工资”和“罚款”按同样权重计入绩效评分——逻辑上荒谬。

1.3 假设三:样本外表现等于样本内表现

这是最隐蔽的假设,也是最危险的假设。

夏普比率是基于历史回测数据计算的。它的隐含逻辑是:过去的表现可以预测未来。但这个假设在以下三种常见情况下会失效:

第一种:幸存者偏差(Survivorship Bias)

你的策略池中有 1000 个策略,最终你只选择表现最好的那个做实盘。但这 1000 个策略中,可能有 800 个已经爆仓或者接近归零。你只看被选中的那一个,自然夏普比率很高。但如果你从一开始就持有全部 1000 个策略,平均夏普可能只有 0.8。

第二种:过拟合(Overfitting)

你用 2018-2023 年的数据不断调参,最终找到一组参数使得样本内夏普达到 3.2。但当你用 2024 年的数据做样本外测试时,夏普跌到了 0.9。

这不是策略失效,这是过拟合的代价——你拟合的是“噪声”而不是“信号”。

第三种:市场机制变化(Regime Change)

你的策略在低利率环境下(2010-2021)的夏普为 2.5,但你没有意识到策略依赖于这个市场环境。当利率上升、波动率结构改变(2022-2024),夏普跌到 0.6。


二、被忽略的关键维度:最大回撤与索提诺比率

理解了夏普的三大假设后,下一个问题自然浮现:什么指标能补充夏普的盲区?

2.1 最大回撤:策略的“生存线”

最大回撤(Maximum Drawdown,MDD)定义是:在任意历史时间点,你的策略账户从峰值跌到谷值的最大百分比。

为什么它重要?

因为它衡量的是“策略在最坏情况下的破坏力”。夏普比率告诉你策略的“平均风险调整收益”,最大回撤告诉你“如果你在错误的时机买入,你最多会亏多少”。

举一个真实的对比案例:

策略 年化收益率 夏普比率 最大回撤
A 25% 1.8 65%
B 12% 1.0 12%

如果你只看夏普,策略 A 完胜。

但如果你在策略 A 的最高点买入 100 万,你会损失 65 万。在数学上恢复这 65 万的损失需要 85% 的收益——这不是随便能做到的。而策略 B 的 12% 最大回撤,即使在最坏的买入时点,也只需要 14% 的恢复收益。

最大回撤的使用原则:

  1. 目标值:个人交易者建议最大回撤 ≤ 25%;机构交易者建议 ≤ 15%
  2. 警戒值:最大回撤 > 40% 的策略需要重新审视策略逻辑
  3. 配合使用:单独看最大回撤没有意义,必须与收益率一起看(Calmar 比率)

Calmar 比率 = 年化收益率 / 最大回撤

Calmar 比率告诉你:你每承受 1% 的最大回撤,能获得多少年化收益。

Calmar 比率 评级
> 2.0 优秀
1.0 - 2.0 良好
0.5 - 1.0 及格
< 0.5

但 Calmar 也有它的局限性:它只看最大回撤,不看回撤恢复速度。

2.2 回撤恢复周期:被遗忘的“时间成本”

一个策略最大回撤 40%,恢复需要多久?

如果你能在恢复期间保持年化 15% 的收益,那么:

恢复周期 = 40% / 15% ≈ 2.67 年

也就是说,你需要承受 2.67 年的心理压力和资金锁定。但如果你有另一个策略,最大回撤只有 20%,年化收益 12%,恢复周期仅为:

恢复周期 = 20% / 12% ≈ 1.67 年

从“资金效率”和“心理成本”角度看,策略 B 可能更适合大多数投资者。

这引出了一个关键认知:

选择策略不仅是选择“收益/风险比”,也是选择“你能承受的资金锁定时间”。

如果你有 5 年的投资周期,两个策略都可以考虑。但如果你只有 1-2 年的资金期限,策略 A 的高回撤+长恢复周期可能让你的资金陷入被动。

2.3 索提诺比率:修复“上行波动”等价的问题

索提诺比率(Sortino Ratio)的计算公式是:

索提诺比率 = (年化收益率 - 目标收益率) / 下行偏差

分母不再是“总波动率”,而是“下行偏差(Downside Deviation)”。下行偏差只计算低于目标收益率的收益的波动,忽略高于目标的收益。

计算步骤:

  1. 设定目标收益率(通常为 0 或无风险利率)
  2. 从每日收益率中减去目标收益率
  3. 筛选出负值(即低于目标的部分)
  4. 对这些负值计算标准差
  5. 用年化超额收益除以下行偏差
import numpy as np

def calculate_sortino_ratio(returns, target_return=0.0):
    """
    计算索提诺比率
    
    Parameters:
    - returns: 每日收益率列表(list 或 numpy array)
    - target_return: 目标收益率(年化),默认 0
    
    Returns:
    - sortino: 索提诺比率(float)
    """
    returns = np.array(returns)
    daily_target = target_return / 252
    
    # 计算日超额收益
    excess_returns = returns - daily_target
    
    # 筛选下行偏差(只取负值)
    downside_returns = excess_returns[excess_returns < 0]
    
    if len(downside_returns) == 0:
        return float('inf')
    
    # 下行偏差(年化)
    downside_deviation = np.std(downside_returns) * np.sqrt(252)
    
    if downside_deviation == 0:
        return float('inf')
    
    # 年化收益率
    annualized_return = np.mean(returns) * 252
    
    # 索提诺比率
    sortino = (annualized_return - target_return) / downside_deviation
    
    return round(sortino, 2)

# 示例
daily_returns = [0.012, -0.008, 0.015, -0.022, 0.009, -0.005, 0.018]
print(f"索提诺比率: {calculate_sortino_ratio(daily_returns)}")

为什么索提诺比夏普更有价值?

场景 夏普比率 索提诺比率 哪个更真实?
趋势策略(趋势期高收益,震荡期反复亏损) 可能较高 较低(下行偏差大) 索提诺
网格策略(稳定小幅盈利,偶尔大幅亏损) 可能尚可 较低(尾部亏损多) 索提诺
做市策略(买卖收益对称,波动均匀) 接近索提诺 接近夏普 两者接近

三、数字说话:夏普 2.0 vs 夏普 1.0 的真实案例对比

3.1 案例设定

假设你在 2020-2023 年的回测中发现两个策略:

指标 策略 A(趋势跟踪) 策略 B(均值回归)
年化收益率 28% 14%
年化波动率 14% 7%
夏普比率 2.0 2.0
最大回撤 52% 11%
Calmar 比率 0.54 1.27
索提诺比率 1.4 2.1
回撤恢复周期 约 3.5 年 约 0.8 年

两个策略的夏普比率完全相同(都是 2.0),但它们的风险特征天差地别。

  • 策略 A:高收益、高波动、高回撤(>50%)、长恢复周期(3.5 年)
  • 策略 B:中收益、低波动、低回撤(<15%)、短恢复周期(0.8 年)

3.2 不同投资者类型的选择

投资者类型 推荐策略 理由
风险承受能力强、资金锁定 5 年以上 A 高夏普+高收益,长期复合增长优势明显
普通个人投资者,资金 1-2 年内需用 B 低回撤+短恢复,心理成本更低
机构投资者,风控严格,回撤上限 15% B 最大回撤 11% 远优于 A 的 52%
资金量小,追求高收益 A 52% 回撤如果能承受,绝对收益更高

这个对比揭示了一个核心结论:

夏普比率相同的两条策略,对不同投资者而言,价值可能天差地别。

你必须把最大回撤、索提诺比率、回撤恢复周期这些指标放进决策框架,而不是只看夏普。

3.3 统计显著性:样本量对夏普的放大效应

还有一个关键问题:夏普比率的可信度与样本量高度相关。

假设你有两个策略,夏普都是 2.0:

策略 样本量(交易日) 回测年份
A 1260 5 年
B 252 1 年

从数字上看,两个策略的夏普都是 2.0。但 B 的夏普置信区间极宽——在 95% 置信度下,它的真实夏普可能落在 0.5 到 3.5 之间。而 A 的真实夏普更可能落在 1.5 到 2.5 之间。

一个实用的经验法则:

  • 回测数据点 ≥ 252(1 年):勉强可用
  • 回测数据点 ≥ 756(3 年):基本可信
  • 回测数据点 ≥ 1260(5 年):较为可靠

更稳妥的做法是:在评估夏普时,同时报告其 95% 置信区间。

def calculate_sharpe_with_confidence(returns, confidence=0.95):
    """
    计算夏普比率及其置信区间
    
    Parameters:
    - returns: 每日收益率列表
    - confidence: 置信水平,默认 95%
    
    Returns:
    - sharpe: 夏普比率(float)
    - ci_low: 置信区间下限
    - ci_high: 置信区间上限
    """
    import scipy.stats as stats
    import numpy as np
    
    returns = np.array(returns)
    n = len(returns)
    
    # 年化收益率和波动率
    annualized_return = np.mean(returns) * 252
    annualized_vol = np.std(returns) * np.sqrt(252)
    
    # 夏普比率
    sharpe = annualized_return / annualized_vol if annualized_vol != 0 else 0
    
    # 标准误(用于置信区间)
    # 夏普的标准误公式:sqrt((1 + (mu^2 / (2 * sigma^2))) / n)
    mu = np.mean(returns)
    sigma = np.std(returns)
    
    if sigma == 0:
        return sharpe, sharpe, sharpe
    
    se = np.sqrt((1 + (mu * 252)**2 / (2 * (sigma * np.sqrt(252))**2)) / n)
    
    # t 分布临界值
    t_crit = stats.t.ppf((1 + confidence) / 2, df=n-1)
    
    # 置信区间
    ci_low = sharpe - t_crit * se
    ci_high = sharpe + t_crit * se
    
    return round(sharpe, 2), round(ci_low, 2), round(ci_high, 2)

# 示例
returns = np.random.normal(0.0004, 0.015, 1260)  # 模拟5年日收益
sharpe, ci_low, ci_high = calculate_sharpe_with_confidence(returns)
print(f"夏普比率: {sharpe} (95% 置信区间: [{ci_low}, {ci_high}])")

四、交易成本与滑点:夏普的隐形杀手

4.1 成本如何吞噬夏普

在回测中,我们往往假设交易成本是固定的(比如 0.1%)。但在实盘中,成本远不止于此:

成本类型 回测假设 实盘现实
佣金 0.02% 0.02% (基本准确)
印花税 0.1% 0.1% (基本准确)
买卖价差 0.05% 0.05% - 0.2%(取决于流动性)
冲击成本 忽略 0.1% - 0.5%(取决于仓位大小)
滑点 忽略 0.1% - 0.5%(极端行情下可达 1%+)

冲击成本(Market Impact)是回测中最容易被低估的成本类型。当你的策略需要买入大量标的时,大单会推动价格向上移动——你买得越多,价格涨得越高,实际成本越大。

一个具体的例子:

假设你的策略每天双边交易 1% 的账户价值,假设:

  • 单笔佣金:0.02%
  • 单笔买卖价差:0.1%
  • 单笔冲击成本:0.15%(假设)
  • 单笔滑点:0.1%(正常行情)/ 0.5%(极端行情)

单笔总成本(正常行情)= 0.02% + 0.1% + 0.15% + 0.1% = 0.37%
单笔总成本(极端行情)= 0.02% + 0.1% + 0.15% + 0.5% = 0.77%

年化影响(250 交易日,双边):

  • 正常行情下:0.37% × 250 × 2 = 1.85%(年化)
  • 极端行情下:0.77% × 250 × 2 = 3.85%(年化)

如果你的策略夏普是 2.0,扣除 1.85% 的年化成本后,夏普可能降到 1.6。如果市场波动加剧,成本可能高达 3.85%,夏普降到 1.2。

4.2 生产环境的成本估算实践

import numpy as np

def estimate_realistic_sharpe(synthetic_sharpe, avg_daily_turnover, 
                              base_commission=0.0002, 
                              spread_cost=0.001,
                              market_impact_factor=0.0005,
                              volatility_regime='normal'):
    """
    估算扣除真实交易成本后的夏普比率
    
    Parameters:
    - synthetic_sharpe: 回测中的夏普比率(未扣成本)
    - avg_daily_turnover: 日均换手率(0.01 = 1%)
    - base_commission: 基础佣金(双向),默认 0.02%
    - spread_cost: 买卖价差(双向),默认 0.1%
    - market_impact_factor: 冲击系数,默认 0.05%
    - volatility_regime: 波动环境 ('normal' / 'high')
    
    Returns:
    - realistic_sharpe: 扣除成本后的估算夏普
    - cost_breakdown: 成本分解
    """
    # 成本系数
    if volatility_regime == 'high':
        market_impact_factor *= 3
        spread_cost *= 1.5
    
    # 每笔交易成本(双边)
    per_trade_cost = (base_commission + spread_cost + 
                      market_impact_factor * avg_daily_turnover)
    
    # 年化成本影响(250交易日,双边)
    annual_cost = per_trade_cost * 250 * 2
    
    # 假设原夏普对应的年化收益
    # 简化:假设年化波动率 15%,夏普 = 收益 / 波动
    # 那么收益 = 夏普 * 波动
    estimated_annual_return = synthetic_sharpe * 0.15
    
    # 扣除成本后的实际收益
    net_return = estimated_annual_return - annual_cost
    
    # 成本后的夏普(假设波动率不变)
    realistic_sharpe = net_return / 0.15
    
    cost_breakdown = {
        'annual_commission': base_commission * 250 * 2,
        'annual_spread_cost': spread_cost * 250 * 2,
        'annual_market_impact': market_impact_factor * avg_daily_turnover * 250 * 2,
        'total_annual_cost': annual_cost
    }
    
    return round(realistic_sharpe, 2), cost_breakdown

# 示例
synthetic_sharpe = 2.0
daily_turnover = 0.01  # 日均换手 1%

# 正常市场
sharpe_normal, costs_normal = estimate_realistic_sharpe(
    synthetic_sharpe, daily_turnover, volatility_regime='normal'
)
print(f"正常市场下估算夏普: {sharpe_normal}")
print(f"  年度佣金: {costs_normal['annual_commission']:.4f} ({costs_normal['annual_commission']*100:.2f}%)")
print(f"  年度价差成本: {costs_normal['annual_spread_cost']:.4f} ({costs_normal['annual_spread_cost']*100:.2f}%)")
print(f"  年度冲击成本: {costs_normal['annual_market_impact']:.4f} ({costs_normal['annual_market_impact']*100:.2f}%)")
print(f"  总成本: {costs_normal['total_annual_cost']:.4f} ({costs_normal['total_annual_cost']*100:.2f}%)")

五、综合评估框架:构建你的策略评分体系

5.1 三层指标体系

把策略评估分为三个层次,每个层次包含核心指标:

第一层:收益与风险(基础)

指标 计算方式 目标值
年化收益率 (终值/初值)^(1/年数) - 1 ≥ 10%
最大回撤 max((peak - trough) / peak) ≤ 25%
日间最大回撤 单日最大亏损 ≤ 8%

第二层:风险调整收益(核心)

指标 计算方式 目标值
夏普比率 (年化收益 - 无风险利率) / 年化波动率 ≥ 1.5
索提诺比率 (年化收益 - 目标收益) / 下行偏差 ≥ 2.0
Calmar 比率 年化收益率 / 最大回撤 ≥ 1.0

第三层:尾部风险与稳健性(补充)

指标 计算方式 目标值
95% VaR 5% 分位数的日收益 ≥ -2%
条件 VaR (CVaR) 尾部损失均值 ≥ -4%
胜率 盈利交易数 / 总交易数 ≥ 55%
盈亏比 平均盈利 / 平均亏损 ≥ 1.5
恢复周期 最大回撤 / 年化收益率 越小越好

5.2 综合评分函数

def comprehensive_strategy_score(returns, target_return=0.0):
    """
    综合策略评分
    
    评分规则:
    - 夏普 >= 2.0: 20分
    - 1.5 <= 夏普 < 2.0: 15分
    - 1.0 <= 夏普 < 1.5: 10分
    - 夏普 < 1.0: 5分
    
    - 最大回撤 <= 15%: 20分
    - 15% < 最大回撤 <= 25%: 15分
    - 25% < 最大回撤 <= 40%: 10分
    - 最大回撤 > 40%: 5分
    
    - 索提诺 >= 2.5: 15分
    - 1.5 <= 索提诺 < 2.5: 10分
    - 索提诺 < 1.5: 5分
    
    - 95% VaR >= -1%: 10分
    - -2% <= 95% VaR < -1%: 8分
    - -3% <= 95% VaR < -2%: 5分
    - 95% VaR < -3%: 2分
    
    - 盈亏比 >= 2.0: 10分
    - 1.5 <= 盈亏比 < 2.0: 8分
    - 1.0 <= 盈亏比 < 1.5: 5分
    - 盈亏比 < 1.0: 2分
    
    - 胜率 >= 60%: 5分
    - 50% <= 胜率 < 60%: 4分
    - 40% <= 胜率 < 50%: 2分
    - 胜率 < 40%: 0分
    
    Returns:
    - score: 综合评分(0-80分)
    - breakdown: 分项得分详情
    """
    import numpy as np
    
    returns = np.array(returns)
    n = len(returns)
    
    # ===== 第一层:夏普 =====
    daily_vol = np.std(returns)
    annualized_vol = daily_vol * np.sqrt(252)
    annual_return = np.mean(returns) * 252
    
    sharpe = annual_return / annualized_vol if annualized_vol > 0 else 0
    
    if sharpe >= 2.0:
        sharpe_score = 20
    elif sharpe >= 1.5:
        sharpe_score = 15
    elif sharpe >= 1.0:
        sharpe_score = 10
    else:
        sharpe_score = 5
    
    # ===== 第一层:最大回撤 =====
    cumulative = (1 + returns).cumprod()
    running_max = np.maximum.accumulate(cumulative)
    drawdowns = (cumulative - running_max) / running_max
    max_dd = abs(drawdowns.min())
    
    if max_dd <= 0.15:
        mdd_score = 20
    elif max_dd <= 0.25:
        mdd_score = 15
    elif max_dd <= 0.40:
        mdd_score = 10
    else:
        mdd_score = 5
    
    # ===== 第二层:索提诺 =====
    excess_returns = returns - (target_return / 252)
    downside_returns = excess_returns[excess_returns < 0]
    downside_dev = np.std(downside_returns) * np.sqrt(252) if len(downside_returns) > 0 else 0
    
    sortino = annual_return / downside_dev if downside_dev > 0 else 0
    
    if sortino >= 2.5:
        sortino_score = 15
    elif sortino >= 1.5:
        sortino_score = 10
    else:
        sortino_score = 5
    
    # ===== 第三层:VaR =====
    var_95 = np.percentile(returns, 5)
    cvar_95 = returns[returns <= var_95].mean()
    
    if var_95 >= -0.01:
        var_score = 10
    elif var_95 >= -0.02:
        var_score = 8
    elif var_95 >= -0.03:
        var_score = 5
    else:
        var_score = 2
    
    # ===== 第三层:盈亏比 =====
    trade_returns = returns  # 简化处理,实际应按交易划分
    wins = returns[returns > 0]
    losses = returns[returns < 0]
    
    avg_win = np.mean(wins) if len(wins) > 0 else 0
    avg_loss = abs(np.mean(losses)) if len(losses) > 0 else 1
    
    win_loss_ratio = avg_win / avg_loss if avg_loss > 0 else 0
    
    if win_loss_ratio >= 2.0:
        wl_score = 10
    elif win_loss_ratio >= 1.5:
        wl_score = 8
    elif win_loss_ratio >= 1.0:
        wl_score = 5
    else:
        wl_score = 2
    
    # ===== 第三层:胜率 =====
    win_rate = len(wins) / n if n > 0 else 0
    
    if win_rate >= 0.60:
        wr_score = 5
    elif win_rate >= 0.50:
        wr_score = 4
    elif win_rate >= 0.40:
        wr_score = 2
    else:
        wr_score = 0
    
    # ===== 综合评分 =====
    total_score = sharpe_score + mdd_score + sortino_score + var_score + wl_score + wr_score
    
    breakdown = {
        '夏普比率': {'score': sharpe_score, 'value': round(sharpe, 2)},
        '最大回撤': {'score': mdd_score, 'value': f"{max_dd*100:.1f}%"},
        '索提诺比率': {'score': sortino_score, 'value': round(sortino, 2)},
        'VaR (95%)': {'score': var_score, 'value': f"{var_95*100:.2f}%"},
        '盈亏比': {'score': wl_score, 'value': round(win_loss_ratio, 2)},
        '胜率': {'score': wr_score, 'value': f"{win_rate*100:.1f}%"}
    }
    
    return total_score, breakdown

# 示例
np.random.seed(42)
simulated_returns = np.random.normal(0.0005, 0.012, 1260)  # 模拟5年日收益
score, breakdown = comprehensive_strategy_score(simulated_returns)

print(f"综合评分: {score}/80")
print("\n分项详情:")
for metric, data in breakdown.items():
    print(f"  {metric}: {data['value']} (得分: {data['score']}/满分)")

5.3 TickDB 在策略回测中的角色

完整的策略评估需要充足的回测数据支撑。使用 TickDB 的历史 K 线数据进行多维度回测:

import os
import requests
import numpy as np

# TickDB 历史 K 线数据获取(用于回测)
def fetch_historical_klines(symbol, interval='1d', limit=1260):
    """
    获取历史 K 线数据用于回测
    
    Parameters:
    - symbol: 交易品种,如 'AAPL.US'
    - interval: K 线周期,如 '1d', '1h', '15m'
    - limit: 获取数量(最大 1000)
    
    Returns:
    - klines: K 线数据列表
    """
    api_key = os.environ.get("TICKDB_API_KEY")
    if not api_key:
        raise ValueError("请设置环境变量 TICKDB_API_KEY")
    
    url = "https://api.tickdb.ai/v1/market/kline"
    headers = {"X-API-Key": api_key}
    params = {
        "symbol": symbol,
        "interval": interval,
        "limit": min(limit, 1000)
    }
    
    try:
        response = requests.get(url, headers=headers, params=params, 
                               timeout=(3.05, 10))
        response.raise_for_status()
        data = response.json()
        
        if data.get("code") != 0:
            error_handlers.get(data.get("code", 0), lambda x: None)(data)
        
        return data.get("data", [])
    except requests.exceptions.Timeout:
        raise TimeoutError("请求超时,请检查网络连接")
    except Exception as e:
        raise RuntimeError(f"数据获取失败: {str(e)}")

# ⚠️ 生产环境建议:
# 1. 对于 5 年日线回测(1260 个数据点),分批获取(每次 1000 点)
# 2. 增加请求间隔(建议 0.2 秒)避免触发限频(code: 3001)
# 3. 使用 tqdm 显示进度条

# 示例使用
try:
    # 获取标普 500 ETF(SPY)5 年日线数据
    klines = fetch_historical_klines("SPY.US", "1d", 1260)
    
    if klines:
        closes = [float(k[4]) for k in klines]  # 收盘价
        returns = np.diff(closes) / closes[:-1]
        
        # 计算各项指标
        score, breakdown = comprehensive_strategy_score(returns)
        print(f"策略综合评分: {score}/80")
        
except ValueError as e:
    print(f"配置错误: {e}")
except TimeoutError:
    print("网络超时,请稍后重试")
except Exception as e:
    print(f"数据处理异常: {e}")

六、结论

回到最初的问题:为什么夏普 2.0 的策略可能不如 1.0?

答案是:夏普 2.0 的策略可能有 60% 的最大回撤、3.5 年的恢复周期、高波动的尾部风险。夏普 1.0 的策略可能有 10% 的最大回撤、0.8 年的恢复周期、更稳健的尾部风险特征。

对于风险承受能力有限、资金期限有限的普通投资者,后者的“有效夏普”可能远高于前者。

夏普比率本身不是陷阱。把单一指标当作决策的唯一依据,才是真正的陷阱。

真正成熟的量化思维是:

  1. 用多维指标体系评估策略,而不是只看夏普
  2. 在回测阶段就考虑成本冲击,而不是实盘后才发现
  3. 用样本外测试验证过拟合风险,而不是盲目相信回测结果
  4. 根据自身风险承受能力和资金期限选择策略,而不是追求纸面上的“最高夏普”

市场不会因为你的夏普高就奖励你,它会因为你在极端行情中活下来而奖励你。

关注极端损失的概率与频率,而不是均值与方差。


下一步行动

如果你想深入理解策略风险评估的实践细节,欢迎阅读 TickDB 的其他技术文章:

  • 《事件驱动策略的回测设计:如何避免幸存者偏差》
  • 《生产级量化系统架构:从数据获取到订单执行》

如果你希望亲手实现本文的指标计算框架

  1. 访问 tickdb.ai 注册(免费,无需信用卡)
  2. 在控制台生成 API Key
  3. 设置环境变量 TICKDB_API_KEY,使用本文代码获取历史 K 线数据
  4. 将你的策略收益序列代入评分函数,生成综合报告

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


风险提示:本文不构成任何投资建议。市场有风险,投资需谨慎。历史回测结果不代表未来表现,策略实际表现可能受市场环境变化、交易成本、流动性等因素影响。建议在实盘前进行充分的样本外测试和压力测试。