🚀 AI 一键生成 joinquant 策略代码

如何在 JoinQuant 回测中计算索提诺比率、卡玛比率等自定义性能指标?

joinquant | 发布时间: 2025/12/19
以下内容由 EasyQuant 生成。

问题描述

如何在回测中计算并展示索提诺比率?

解决方案

在JoinQuant(聚宽)的回测详情页中,系统会在回测结束后自动计算并展示“索提诺比率(Sortino Ratio)”。但如果您希望在回测过程中实时计算并以曲线形式展示该指标,需要在策略代码中手动实现计算逻辑,并使用 record() 函数进行绘制。

核心逻辑

  1. 数据收集:我们需要记录每一天的日收益率
  2. 下行波动率计算:索提诺比率的核心在于分母是下行偏差(Downside Deviation),即只考虑低于目标收益(通常为0或无风险利率)的波动。
  3. 公式
    $$ \text{Sortino Ratio} = \frac{\text{年化收益率} - \text{无风险利率}}{\text{年化下行波动率}} $$
  4. 展示:在 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

代码关键点解析

  1. g.daily_returns: 我们使用一个全局列表来存储每一天的 context.portfolio.daily_return。这是计算历史波动率的基础。
  2. 下行偏差计算 (np.clip): 索提诺比率与夏普比率最大的区别在于分母。夏普使用总波动率(标准差),而索提诺只关心“坏的波动”。代码中 np.clip(..., -np.inf, 0) 的作用是将所有正收益置为0,只保留负收益来计算标准差。
  3. 年化处理:
    • 收益率年化:日均收益 * 250
    • 波动率年化:日波动率 * sqrt(250)
  4. 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 命名的曲线。