问题描述
解决方案
索提诺比率 (Sortino Ratio) 计算方法与 QMT 实现
索提诺比率(Sortino Ratio)是一个衡量投资组合经风险调整后收益的指标。与夏普比率(Sharpe Ratio)不同,索提诺比率仅考虑下行风险(Downside Risk),即只惩罚低于目标回报率(通常为0或无风险利率)的波动,而忽略上行波动(盈利带来的波动)。这使得它对于不希望亏损的投资者来说,是一个比夏普比率更贴切的评估指标。
1. 计算公式
$$ \text{Sortino Ratio} = \frac{R_p - R_f}{\sigma_d} $$
其中:
- $R_p$:投资组合的预期年化收益率。
- $R_f$:无风险利率(Risk-Free Rate)。
- $\sigma_d$:下行标准差(Downside Deviation),即低于最小可接受回报率(MAR,通常设为0)的收益率的标准差。
2. QMT Python 代码实现
以下是一个完整的 QMT 策略代码示例。该策略在 init 中初始化,在 handlebar 中获取指定标的的历史数据,计算日收益率,并最终计算出索提诺比率。
# -*- coding: gbk -*-
import pandas as pd
import numpy as np
def init(ContextInfo):
# 设置要计算的标的,例如沪深300ETF
ContextInfo.stock_code = '510300.SH'
# 设置无风险利率 (年化),例如 3%
ContextInfo.risk_free_rate = 0.03
# 设置最小可接受回报率 (MAR),用于计算下行风险,通常为 0
ContextInfo.target_return = 0.0
# 设置计算的时间窗口(例如过去252个交易日)
ContextInfo.lookback_window = 252
def handlebar(ContextInfo):
# 为了避免每个tick都计算,只在K线结束时计算
if not ContextInfo.is_last_bar():
return
# 1. 获取历史行情数据
# 使用 get_market_data_ex 获取更丰富的数据结构
# 获取收盘价,长度为 lookback_window + 1 (为了计算收益率)
market_data = ContextInfo.get_market_data_ex(
fields=['close'],
stock_code=[ContextInfo.stock_code],
period='1d',
count=ContextInfo.lookback_window + 1,
dividend_type='front', # 前复权,保证收益率计算准确
subscribe=False
)
if ContextInfo.stock_code not in market_data:
print(f"未获取到 {ContextInfo.stock_code} 的数据")
return
df = market_data[ContextInfo.stock_code]
# 2. 计算日收益率
# pct_change() 计算 (今日收盘 - 昨日收盘) / 昨日收盘
df['daily_return'] = df['close'].pct_change()
# 去除第一行 NaN
df.dropna(inplace=True)
if len(df) == 0:
print("数据不足,无法计算")
return
# 3. 计算索提诺比率 (Sortino Ratio)
sortino = calculate_sortino_ratio(
df['daily_return'],
ContextInfo.risk_free_rate,
ContextInfo.target_return
)
# 4. 输出结果
print("=" * 30)
print(f"标的: {ContextInfo.stock_code}")
print(f"时间窗口: {len(df)} 交易日")
print(f"索提诺比率 (Sortino Ratio): {sortino:.4f}")
print("=" * 30)
def calculate_sortino_ratio(daily_returns, risk_free_rate_annual, target_return=0):
"""
计算索提诺比率
:param daily_returns: pandas Series, 日收益率序列
:param risk_free_rate_annual: float, 年化无风险利率 (如 0.03)
:param target_return: float, 最小可接受回报率 (通常为0)
:return: float, 索提诺比率
"""
# 将年化无风险利率转换为日度无风险利率 (按252个交易日近似)
rf_daily = risk_free_rate_annual / 252
# A. 计算年化超额收益 (Annualized Excess Return)
# 策略的平均日收益率
mean_daily_return = daily_returns.mean()
# 年化策略收益
annualized_return = mean_daily_return * 252
# 年化超额收益 (分子)
excess_return = annualized_return - risk_free_rate_annual
# B. 计算下行偏差 (Downside Deviation)
# 筛选出低于目标回报率的收益 (通常是负收益)
# 注意:这里计算的是相对于 target_return 的下行波动
downside_diff = daily_returns - target_return
# 只保留小于0的部分,大于0的部分置为0 (因为索提诺只惩罚下行)
downside_diff = np.where(downside_diff < 0, downside_diff, 0)
# 计算下行标准差 (根号下(平方和的均值))
downside_variance = np.mean(downside_diff ** 2)
downside_deviation_daily = np.sqrt(downside_variance)
# 年化下行标准差
downside_deviation_annual = downside_deviation_daily * np.sqrt(252)
# C. 计算比率
# 避免除以0
if downside_deviation_annual == 0:
return np.nan
sortino_ratio = excess_return / downside_deviation_annual
return sortino_ratio
3. 代码逻辑解析
-
数据获取 (
get_market_data_ex):- 我们获取了指定标的(如
510300.SH)过去一年的日线收盘价。 - 关键点:必须使用复权数据 (
dividend_type='front'),否则分红派息会导致价格跳空,从而错误地计算出巨大的负收益,严重扭曲索提诺比率。
- 我们获取了指定标的(如
-
收益率计算:
- 使用 Pandas 的
pct_change()计算每日的涨跌幅。
- 使用 Pandas 的
-
下行偏差计算 (
calculate_sortino_ratio函数):- 核心逻辑:
np.where(downside_diff < 0, downside_diff, 0)。这一步将所有盈利(或高于目标回报)的日子视为 0 波动。 - 这意味着,如果你的策略经常大涨,这些大涨带来的波动不会降低索提诺比率(而在夏普比率中,大涨带来的波动会增加分母,从而降低比率)。
- 只有亏损(或低于目标)的日子才会被计入风险。
- 核心逻辑:
-
年化处理:
- 收益率乘以 252。
- 下行标准差乘以 $\sqrt{252}$。
4. 常见问题 (Q&A)
Q1: 索提诺比率和夏普比率哪个更好?
A: 没有绝对的更好。夏普比率是行业标准,适合评估整体波动性。但对于趋势跟踪策略或期权策略(具有不对称收益分布),索提诺比率更好,因为它不惩罚“向上的波动”。如果你的策略目标是稳健增值且极度厌恶回撤,应优先参考索提诺比率。
Q2: 为什么我的索提诺比率显示为 nan 或无穷大?
A: 如果在计算周期内,策略没有任何一天的收益率低于目标回报率(即没有下行波动),分母为 0,结果就会是无穷大。这通常发生在回测周期太短或者策略在单边大牛市中表现极好的情况下。
Q3: target_return 应该设为 0 还是无风险利率?
A: 业界最常用的做法是设为 0。这意味着我们只把“亏钱”视为风险。如果你将 target_return 设为无风险利率(如 3%),意味着只要跑不赢理财产品,就被视为风险,这是一种更严格的标准。
Q4: QMT 回测报告里自带索提诺比率吗?
A: 是的。在 QMT 的回测结果面板中,绩效分析栏目通常会自动计算并显示索提诺比率。上述代码主要用于你在编写策略逻辑内部(例如根据索提诺比率动态调整仓位)或进行自定义分析时使用。