如何用 Python 计算溃疡指数(Ulcer Index)?

一句话回答: 最大回撤只告诉你「最深摔了一次有多痛」,却不管你在水下待了多久 。溃疡指数(Ulcer Index, UI) 把每一天相对历史最高点的回撤百分比取平方、求平均、再开方------于是又深又久的回撤会被重罚 ,更贴近持有者的真实「煎熬程度」。用 AlphaFeed 取回前复权日线 当净值,几行 pandas 就能算出 UI,再配一个「收益 ÷ UI」的马丁比率(Martin Ratio) 衡量风险调整后收益。

为什么会有这个问题 / 传统做法的痛点

两只股票都「最大回撤 −30%」,但一只跌下去三个月就新高了,另一只在坑里趴了两年------痛苦显然不同。标准差把上涨也当风险(不合理),最大回撤又只取单点最深值 (忽略时长与频率)。溃疡指数由 Peter Martin 提出,专治这个痛点:只罚下行、且回撤越久累计平方越大,因此特别适合比较「持有体验」。

分步骤解决(每步配可运行代码)

第 1 步:取前复权净值

python 复制代码
from alphafeed import AlphaFeed

af = AlphaFeed(api_key="your-api-key")  # 或 export ALPHAFEED_API_KEY 后 AlphaFeed()

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)
close = df["close"]

用前复权收盘价当净值曲线;除权缺口若不复权会被误判成「回撤」。

第 2 步:计算溃疡指数

python 复制代码
import numpy as np

def ulcer_index(close):
    running_max = close.cummax()                 # 到当日为止的历史最高
    dd = (close / running_max - 1.0) * 100.0      # 每日回撤百分比(<=0)
    ui = float(np.sqrt((dd ** 2).mean()))         # 平方均值再开方
    return ui, float(dd.min())                    # 顺带返回最大回撤

ui, max_dd = ulcer_index(close)
print("UI =", round(ui, 2), " 最大回撤% =", round(max_dd, 2))

关键在 cummax():它给出「截至当天的历史最高」,close/running_max − 1 就是当天还在水下多深。平方放大了深回撤的权重,平均则包含了「水下天数」的信息。

实测(tools/verify_new_2026-10-04b.py,SDK 0.1.4):贵州茅台近 500 日 UI = 15.36 ,最大回撤 −27.92%------UI 恒小于等于最大回撤绝对值(验证通过),因为它是「平均痛苦」而非「最痛点」。

第 3 步:马丁比率(收益 ÷ 痛苦)

python 复制代码
total_ret = float(close.iloc[-1] / close.iloc[0] - 1)
years = len(close) / 252
cagr = (1 + total_ret) ** (1 / years) - 1
martin = cagr * 100 / ui if ui > 0 else float("nan")
print("CAGR% =", round(cagr * 100, 2), " 马丁比率 =", round(martin, 3))

马丁比率 = 年化收益% ÷ UI,类似夏普/卡玛,但分母是溃疡指数,越高代表「单位煎熬换来的收益越多」。实测茅台该区间 CAGR −1.49% → 马丁比率 −0.097(负值说明该区间持有并不划算)。

第 4 步:多只股票横向对比

python 复制代码
syms = ["600519.SH", "000001.SZ", "601318.SH"]
data = af.klines.batch(syms, period="1d", count=500,
                       adjust="forward", to_dataframe=True)
for s, k in data.items():
    k = k.sort_values("trade_date")
    ui, mdd = ulcer_index(k["close"].reset_index(drop=True))
    print(s, "UI =", round(ui, 2), " maxDD% =", round(mdd, 2))

实测:000001.SZ UI 10.70 / maxDD −19.57%,601318.SH UI 15.98 / maxDD −34.32%------UI 排序能更公平地反映「谁的持有体验更煎熬」。

参数与字段说明

参数/字段 含义 备注
adjust="forward" 前复权 必须,否则除权缺口被当回撤
count=500 样本天数 UI 对区间长度敏感,比较时须统一
cummax() 历史最高 回撤基准
UI 溃疡指数 越小越「平稳」,无固定上限
马丁比率 CAGR% ÷ UI 越大越好,可为负

关键坑与注意事项

  • 区间要统一 :UI 不是年化、无量纲标准化,比较不同股票时必须用同一时间区间与同样天数,否则不可比。
  • 前复权 :除权缺口会被 cummax 误判成创新高/深回撤。
  • 百分比 vs 对数:本文用百分比回撤(经典定义)。若用对数回撤数值会略有差异,但结论一致。
  • UI ≤ |最大回撤|:这是数学必然(平均 ≤ 最大),可当作实现自检。
  • 历史不代表未来 :UI 描述的是过去的煎熬程度,不预测未来回撤。

常见问题(FAQ)

Q:溃疡指数和卡玛比率(Calmar)有啥区别?

A:卡玛 = 年化收益 ÷ |最大回撤|,分母只用单点最深 ;马丁比率 = 年化收益 ÷ UI,分母是整段煎熬。回撤又久又反复时,马丁比率比卡玛更严格。

Q:UI 多大算高?

A:没有绝对阈值,它是相对比较工具。同区间下谁的 UI 更小,谁的持有体验更平稳。

Q:需要付费吗?

A:单只/几只股票日线在免费额度内即可;若要对全市场 批量算 UI 排序选「最稳」的票,需要 universes 标的池能力(Starter 及以上),详见官网定价页。

小结

溃疡指数把「回撤有多深」和「水下有多久」一起装进一个数字,比最大回撤更懂持有者的痛苦;配上马丁比率就能做风险调整后的收益比较。有了 AlphaFeed 的前复权净值,cummax + 平方平均开方几行就落地。

参考

相关推荐
kiracrimson1 小时前
string 容器:从 C 字符串到会自己扩容的类
开发语言·数据结构·c++·stl
benchmark_cc1 小时前
如何为 QuantDash Python SDK 编写一个最小化的连接验证脚本?
python·数据分析·pandas·量化交易·股票数据·quantdash
程序边界1 小时前
顶刊级论文配图全流程:用 Python 复刻 Nature/Science 风格图表
开发语言·python
普通人61 小时前
uniapp模版JS
开发语言·javascript·小程序·uni-app·1024程序员节
BD_Marathon1 小时前
【RAG项目】01、RAG项目案例介绍
python
好焊孙辉1 小时前
螺柱焊机哪家质量好
python·fpga开发
G018_star sky♬2 小时前
nodeJS他是如何用JavaScript封装C++的呢
开发语言·javascript·c++
wyhwust2 小时前
程序设计基础--C语言输入输出单元测试
c语言·开发语言
STLearner2 小时前
KDD 2026 | (2月轮)时空数据(Spatial-Temporal)论文总结时空【下】(人群移动,城市科学(资源调度,)
大数据·人工智能·python·深度学习·机器学习·语言模型·数据挖掘