问题描述
解决方案
在PTrade中,虽然回测结束后系统会自动生成绩效报告,但在策略运行过程中(尤其是实盘或模拟盘),你可能希望每天收盘后实时了解当前的风险指标。
要实现这一功能,我们需要在策略代码中手动维护一个“历史净值列表”或“历史收益率列表”,并在每天盘后(after_trading_end)进行计算。
核心逻辑说明
-
最大回撤 (Max Drawdown):
- 需要维护一个变量
g.max_portfolio_value记录历史最高总资产。 - 每日收盘计算:
1 - (当前总资产 / 历史最高总资产)。
- 需要维护一个变量
-
夏普比率 (Sharpe Ratio):
- 需要维护一个列表
g.daily_returns记录每天的收益率。 - 公式:
(平均日收益率 - 日无风险利率) / 日收益率标准差 * sqrt(252)。 - 注:为了简化计算,通常将无风险利率设为0或固定值。
- 需要维护一个列表
完整策略代码示例
以下代码展示了如何在一个简单的策略中嵌入这两个指标的计算。你可以将 initialize 中的变量初始化和 after_trading_end 中的计算逻辑复制到你自己的策略中。
import numpy as np
def initialize(context):
# 1. 设置示例股票池(仅作演示,可替换为你自己的逻辑)
g.security = '600570.SS'
set_universe(g.security)
# 2. 初始化统计所需的全局变量
# 记录每日收益率的列表
g.daily_returns = []
# 记录上一个交易日的总资产,初始化为起始资金
g.last_portfolio_value = context.portfolio.portfolio_value
# 记录历史最高总资产,初始化为起始资金
g.max_portfolio_value = context.portfolio.portfolio_value
# 设定无风险利率 (例如年化 3%),用于夏普比率计算
g.risk_free_rate_annual = 0.03
def handle_data(context, data):
# --- 简单的买入持有逻辑用于演示 ---
# 如果没有持仓,则全仓买入
if context.portfolio.positions[g.security].amount == 0:
order_value(g.security, context.portfolio.cash)
def after_trading_end(context, data):
"""
盘后运行函数,用于计算并记录当天的夏普比率和最大回撤
"""
# 获取当前账户总资产
current_value = context.portfolio.portfolio_value
# --- 1. 计算并更新最大回撤 ---
# 更新历史最高资产
if current_value > g.max_portfolio_value:
g.max_portfolio_value = current_value
# 计算当前回撤
drawdown = 0
if g.max_portfolio_value > 0:
drawdown = 1 - (current_value / g.max_portfolio_value)
# --- 2. 计算并更新夏普比率 ---
# 计算当日收益率
if g.last_portfolio_value > 0:
daily_ret = (current_value / g.last_portfolio_value) - 1
g.daily_returns.append(daily_ret)
# 更新昨日资产,供明天使用
g.last_portfolio_value = current_value
# 计算夏普比率 (至少需要2天数据才能计算标准差)
sharpe_ratio = 0.0
if len(g.daily_returns) > 1:
# 将年化无风险利率转换为日无风险利率
rf_daily = g.risk_free_rate_annual / 252.0
# 计算收益率的均值和标准差 (ddof=1 表示样本标准差)
avg_return = np.mean(g.daily_returns)
std_return = np.std(g.daily_returns, ddof=1)
# 防止标准差为0导致除零错误
if std_return > 0:
# 年化夏普 = (日均收益 - 日无风险利率) / 日标准差 * sqrt(252)
sharpe_ratio = ((avg_return - rf_daily) / std_return) * np.sqrt(252)
# --- 3. 打印日志 ---
# 格式化输出,保留小数位
log.info("【绩效监控】日期: %s, 当前总资产: %.2f" % (
str(context.blotter.current_dt), current_value
))
log.info("【绩效监控】最大回撤: %.2f%%, 夏普比率: %.4f" % (
drawdown * 100, sharpe_ratio
))
代码详解
-
初始化 (
initialize):- 我们引入了
g.daily_returns来存储每天的收益率数据流。 g.max_portfolio_value用于“锚定”历史最高点,这是计算回撤的基础。
- 我们引入了
-
盘后计算 (
after_trading_end):- 最大回撤: 逻辑非常直接,即
(最高点 - 当前点) / 最高点。如果当前点创新高,回撤为0。 - 夏普比率:
- 首先计算当天的简单收益率:
(今天净值 / 昨天净值) - 1。 - 使用
numpy计算历史收益率序列的平均值 (mean) 和标准差 (std)。 - 年化处理: 夏普比率通常是年化的,因此我们将结果乘以 $\sqrt{252}$(假设一年有252个交易日)。
- 无风险利率: 代码中简单地将年化3%平摊到每天。如果你想计算“信息比率”或不考虑无风险利率,可以将
rf_daily设为 0。
- 首先计算当天的简单收益率:
- 最大回撤: 逻辑非常直接,即
-
兼容性:
- 代码使用了 Python 3.5 兼容的字符串格式化
%语法,确保在 PTrade 旧版本环境中也能稳定运行。 - 使用了
numpy库,这是 PTrade 环境内置支持的标准科学计算库。
- 代码使用了 Python 3.5 兼容的字符串格式化
Q&A
Q1: 为什么计算出的夏普比率和PTrade回测报告里的不完全一样?
A: 可能有细微差异。原因包括:1. 无风险利率的取值不同(PTrade系统可能默认设为0或其它值);2. 收益率计算方式(简单收益率 vs 对数收益率);3. 年化因子的处理(有些系统按365天,有些按250或252天)。上述代码使用的是标准的年化252天计算法。
Q2: 这个代码会影响策略运行速度吗?
A: 几乎不会。after_trading_end 每天只运行一次,且 numpy 的计算效率极高。即使运行数年积累了上千个数据点,计算耗时也是毫秒级的。
Q3: 如果我想计算最近30天的滚动夏普比率怎么办?
A: 你可以在计算 np.mean 和 np.std 时,对列表进行切片。例如:
recent_returns = g.daily_returns[-30:] # 取最后30个数据
avg_return = np.mean(recent_returns)
# ...后续计算相同