问题描述
解决方案
在JoinQuant(聚宽)的回测详情页中,系统会在回测结束后自动计算并展示“索提诺比率(Sortino Ratio)”。但如果您希望在回测过程中实时计算并以曲线形式展示该指标,需要在策略代码中手动实现计算逻辑,并使用 record() 函数进行绘制。
核心逻辑
- 数据收集:我们需要记录每一天的日收益率。
- 下行波动率计算:索提诺比率的核心在于分母是下行偏差(Downside Deviation),即只考虑低于目标收益(通常为0或无风险利率)的波动。
- 公式:
$$ \text{Sortino Ratio} = \frac{\text{年化收益率} - \text{无风险利率}}{\text{年化下行波动率}} $$ - 展示:在
after_trading_end函数中调用record()。
策略代码实现
以下是一个完整的策略示例,包含了一个简单的均线策略作为交易逻辑,重点在于 after_trading_end 中的索提诺比率计算与展示。
# -*- coding: utf-8 -*-
import numpy as np
def initialize(context):
# 设定沪深300作为基准
set_benchmark('000300.XSHG')
# 开启动态复权模式(真实价格)
set_option('use_real_price', True)
# 设定手续费
set_order_cost(OrderCost(close_tax=0.001, open_commission=0.0003, close_commission=0.0003, min_commission=5), type='stock')
# 定义一个全局变量,用于存储每日收益率
g.daily_returns = []
# 设定无风险利率 (例如 3%)
g.risk_free_rate = 0.03
# 设定要操作的股票
g.security = '000001.XSHE'
# 每天开盘时运行交易逻辑
run_daily(market_open, time='09:30')
# 每天收盘后计算指标
run_daily(after_trading_end, time='15:30')
def market_open(context):
"""
简单的交易逻辑:用于产生收益波动,以便计算指标
"""
security = g.security
# 获取过去5天的收盘价
close_data = attribute_history(security, 5, '1d', ['close'])
ma5 = close_data['close'].mean()
current_price = close_data['close'][-1]
# 简单的均线策略
if current_price > ma5 and len(context.portfolio.positions) == 0:
order_value(security, context.portfolio.available_cash)
elif current_price < ma5 and len(context.portfolio.positions) > 0:
order_target(security, 0)
def after_trading_end(context):
"""
收盘后计算索提诺比率并绘图
"""
# 1. 获取当日收益率
# context.portfolio.daily_return 是当日的收益率 (例如 0.01 代表 1%)
today_return = context.portfolio.daily_return
# 2. 添加到历史记录中
g.daily_returns.append(today_return)
# 3. 计算索提诺比率 (需要至少有几天的数据才有意义)
if len(g.daily_returns) > 10:
sortino = calculate_sortino_ratio(g.daily_returns, g.risk_free_rate)
# 4. 使用 record 在图表中展示
# 注意:record 的 key 必须是英文
record(Sortino_Ratio=sortino)
def calculate_sortino_ratio(returns_list, rf):
"""
计算索提诺比率的辅助函数
:param returns_list: 日收益率列表
:param rf: 年化无风险利率
:return: 索提诺比率
"""
# 转换为 numpy 数组方便计算
returns_np = np.array(returns_list)
# --- 分子:年化超额收益 ---
# 计算平均日收益率
avg_daily_return = np.mean(returns_np)
# 简单年化收益率 = 日均收益 * 250
annualized_return = avg_daily_return * 250
# 超额收益
excess_return = annualized_return - rf
# --- 分母:年化下行波动率 ---
# 目标收益率转换为日频 (通常取0或者无风险利率/250)
target_daily_return = 0
# 只保留低于目标收益的部分(下行偏差),高于目标的部分视为0
# clip(a, max_value, min_value) 这里我们将大于0的收益置为0,只保留负收益
downside_returns = np.clip(returns_np - target_daily_return, -np.inf, 0)
# 计算下行标准差 (Downside Deviation)
# dd = sqrt(mean(downside_returns^2))
downside_deviation = np.sqrt(np.mean(np.square(downside_returns)))
# 年化下行波动率
annualized_downside_deviation = downside_deviation * np.sqrt(250)
# --- 计算比率 ---
# 避免分母为0
if annualized_downside_deviation == 0:
return 0
return excess_return / annualized_downside_deviation
代码关键点解析
g.daily_returns: 我们使用一个全局列表来存储每一天的context.portfolio.daily_return。这是计算历史波动率的基础。- 下行偏差计算 (
np.clip): 索提诺比率与夏普比率最大的区别在于分母。夏普使用总波动率(标准差),而索提诺只关心“坏的波动”。代码中np.clip(..., -np.inf, 0)的作用是将所有正收益置为0,只保留负收益来计算标准差。 - 年化处理:
- 收益率年化:
日均收益 * 250 - 波动率年化:
日波动率 * sqrt(250)
- 收益率年化:
record(): 该函数会将计算出的数值绘制在回测结果页面的收益曲线下方。您可以在回测运行时的“收益图”下方看到名为Sortino_Ratio的曲线。
Q&A
Q1: 索提诺比率和夏普比率有什么区别?
A1: 夏普比率(Sharpe Ratio)惩罚所有波动,包括上涨带来的波动;而索提诺比率(Sortino Ratio)只惩罚下行波动(亏损风险)。对于追求高收益且不介意上涨波动的策略,索提诺比率通常被认为更合理。
Q2: 为什么回测刚开始时索提诺比率可能显示为0或波动剧烈?
A2: 计算标准差需要一定数量的样本。在回测初期(例如前10天),由于样本量太少,统计学上的方差不稳定,导致比率计算结果不具备参考性。代码中加入了 if len(g.daily_returns) > 10 的判断来缓解这个问题。
Q3: record() 函数画出的图在哪里看?
A3: 在聚宽回测详情页,收益率曲线图的下方会有一个区域显示自定义指标。如果您在代码中使用了 record(Key=Value),这里就会出现一条以 Key 命名的曲线。