一句话回答: 夏普比率把「上涨的波动」也算成风险,而索提诺比率(Sortino)只惩罚下行波动 、卡玛比率(Calmar)用最大回撤 当风险分母------更贴合投资者「怕跌不怕涨」的真实感受。用 AlphaFeed 取回前复权日线算日收益后,几行 pandas 就能得到年化收益、下行波动、最大回撤,进而算出 Sortino 与 Calmar,不需要专门的库。
为什么会有这个问题 / 传统做法的痛点
只看夏普比率有个尴尬:一只票暴涨也会推高波动率、反而拉低夏普,可上涨明明是好事。索提诺只把「跌」当风险,卡玛直接盯「最大回撤」,二者更能反映实际持有体验。计算的坑在于:要用前复权价 算真实收益,注意年化口径(A 股一年约 252 个交易日),下行波动只统计负收益。
分步骤解决(每步配可运行代码)
第 1 步:取前复权日线并算日收益
python
from alphafeed import AlphaFeed
import numpy as np
import pandas as pd
af = AlphaFeed() # 读取 ALPHAFEED_API_KEY
df = af.klines.get("600519.SH", period="1d", count=500,
adjust="forward", to_dataframe=True)
df = df.sort_values("trade_date").reset_index(drop=True)
ret = df["close"].pct_change().dropna()
用前复权价(
adjust="forward")算收益,避免除权日的伪涨跌污染指标。
第 2 步:夏普 vs 索提诺(下行波动的差别)
python
TRADING_DAYS = 252
rf_daily = 0.0 # 简化:无风险利率按 0
def sharpe(ret, rf=0.0):
excess = ret - rf
return np.sqrt(TRADING_DAYS) * excess.mean() / excess.std(ddof=1)
def sortino(ret, rf=0.0):
excess = ret - rf
downside = excess[excess < 0] # 只统计下行
dd_std = np.sqrt((downside ** 2).mean()) # 下行标准差
return np.sqrt(TRADING_DAYS) * excess.mean() / dd_std
print(f"夏普 {sharpe(ret):.2f} | 索提诺 {sortino(ret):.2f}")
| 指标 | 风险分母 | 特点 |
|---|---|---|
| 夏普 Sharpe | 全部收益的标准差 | 上涨波动也算"风险" |
| 索提诺 Sortino | 仅下行收益的标准差 | 只罚下跌,更符合直觉 |
第 3 步:卡玛比率(年化收益 / 最大回撤)
python
def cagr(close):
total = close.iloc[-1] / close.iloc[0]
years = len(close) / TRADING_DAYS
return total ** (1 / years) - 1
def max_drawdown(close):
equity = close / close.iloc[0]
return (equity / equity.cummax() - 1).min() # 负值
c = cagr(df["close"])
mdd = max_drawdown(df["close"])
calmar = c / abs(mdd) if mdd != 0 else np.nan
print(f"年化收益 {c:.2%} | 最大回撤 {mdd:.2%} | 卡玛 {calmar:.2f}")
| 指标 | 公式 | 解读 |
|---|---|---|
| 卡玛 Calmar | 年化收益 ÷ |最大回撤| | 每承受 1 单位回撤换来的年化收益 |
| 越大越好 | --- | 通常看近 3 年;>1 较优(因品种而异) |
第 4 步:批量对比多只/多策略
python
codes = ["600519.SH", "000001.SZ", "601318.SH", "000858.SZ"]
data = af.klines.batch(codes, period="1d", count=500,
adjust="forward", to_dataframe=True)
rows = []
for s, k in data.items():
k = k.sort_values("trade_date"); r = k["close"].pct_change().dropna()
rows.append({"symbol": s, "Sharpe": round(sharpe(r), 2),
"Sortino": round(sortino(r), 2),
"Calmar": round(cagr(k["close"]) / abs(max_drawdown(k["close"])), 2)})
print(pd.DataFrame(rows).to_string(index=False))
关键坑与注意事项
- 前复权价:未复权价在除权日的伪跳变会污染收益与所有比率。
- 年化口径统一:A 股按 252 交易日年化;日/周/月频率的年化因子不同,别混用。
- 下行波动只统计负收益:Sortino 的分母是负收益的均方根,别用全样本 std。
- 卡玛对区间长度敏感:最大回撤随样本变化大,通常固定看近 3 年、且需足够样本。
- 无风险利率简化为 0:更严谨可减去货币基金/国债日收益,但对横向对比影响小。
- 单指标不足以定优劣:三个比率各有侧重,应结合收益、回撤、稳定性综合看,且历史不代表未来。
常见问题(FAQ)
Q:索提诺一定比夏普高吗?
A:通常是,因为它剔除了上行波动这个「假风险」。若下跌集中且剧烈,索提诺也可能不高。
Q:卡玛比率多少算好?
A:没有绝对标准,通常 >1 较优、越大越好,但严重依赖回测区间与品种,跨样本对比才有意义。
Q:为什么用 252?
A:A 股一年约 252 个交易日,是日频年化的常用因子;用月频则年化因子是 12。
Q:需要付费吗?
A:单只历史日线有免费额度;批量对比多只/组合(klines.batch + universes)需 Starter 及以上。详见定价页。
小结
评估收益别只看夏普:索提诺只罚下行波动、卡玛用最大回撤当分母,更贴合真实风险偏好。用 AlphaFeed 取回前复权日线算日收益,几行 pandas 就能得到 Sharpe/Sortino/Calmar 三件套。关键是前复权、统一年化口径、下行波动只算负收益,并结合多指标综合判断。
参考
- AlphaFeed Python SDK 文档:https://docs.alphafeed.org/zh-Hans/sdk/python-quickstart
- AlphaFeed 官网:https://alphafeed.org/