3 分钟快速生成代码
输入想法,AI 即刻生成可运行代码
在JoinQuant(聚宽)的回测详情页中,系统会在回测结束后自动计算并展示“索提诺比率(Sortino Ratio)”。但如果您希望在回测过程中实时计算并以曲线形式展示该指标,需要在策略代码中手动实现计算逻辑,并使用 record() 函数进行绘制。
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 的曲线。Q1: 索提诺比率和夏普比率有什么区别?
A1: 夏普比率(Sharpe Ratio)惩罚所有波动,包括上涨带来的波动;而索提诺比率(Sortino Ratio)只惩罚下行波动(亏损风险)。对于追求高收益且不介意上涨波动的策略,索提诺比率通常被认为更合理。
Q2: 为什么回测刚开始时索提诺比率可能显示为0或波动剧烈?
A2: 计算标准差需要一定数量的样本。在回测初期(例如前10天),由于样本量太少,统计学上的方差不稳定,导致比率计算结果不具备参考性。代码中加入了 if len(g.daily_returns) > 10 的判断来缓解这个问题。
Q3: record() 函数画出的图在哪里看?
A3: 在聚宽回测详情页,收益率曲线图的下方会有一个区域显示自定义指标。如果您在代码中使用了 record(Key=Value),这里就会出现一条以 Key 命名的曲线。