← 返回量化与 AI 投研概念库

怎么看一条收益曲线:别只看终点,看它的形状

最后更新 2026-08-18
R2 · 注意风险

有人发给你一张图。一条曲线从左下角一路爬到右上角,右边标着「年化 63%」。你看了三秒,心里已经有了判断。

问题是,那三秒里你看的是终点的高度,而这条曲线上真正有信息的东西,几乎全在别的地方——它是怎么爬上去的,中间塌过几次,塌下去之后趴了多久,那几段特别陡的地方发生在哪几天,以及有没有哪一处的形状根本不像市场能画出来的样子。

同一个终点,可以对应完全不同的经历。有一条是每个月稳稳赚一点点,有一条是四年横盘、第五年靠三笔翻身。它们的年化数字可能一模一样,但你拿真钱去经历它们,是两种完全不同的人生。

先说人话:它是病历,不是成绩单

想象两个人跑完了同一场马拉松,成绩都是四小时整。

第一个人从头到尾配速稳定,每公里都在同一个节奏上。第二个人前十公里飞快,中间三十公里两次抽筋停下来走,最后靠冲刺把时间追了回来。四小时整这个数字,把这两段完全不同的经历压成了同一个字符。

看收益曲线的人,绝大多数只看到了「四小时整」。

曲线本身就是那份配速表。它把每一段时间里发生了什么,按顺序摊开在你面前。你要做的不是欣赏它的高度,是像医生看心电图一样看它的形状——哪一段规律,哪一段异常,哪一处的跳变根本不是人体能产生的。

这个思路一旦建立起来,你会发现读一条曲线是有固定顺序的。下面这五步,从最基础的往上走。

第一步:先问纵轴是什么

这是最容易被跳过、也最容易被利用的一步。同一份收益记录,可以画出好几条长得完全不一样的曲线,区别只在于纵轴的口径。

累加还是复利 把每天的涨跌幅直接加起来,和把它们连乘起来(也就是利滚利),得到的是两条形状不同的线。复利那条在后期会越翘越夸张——因为本金变大之后,同样的百分比对应更大的绝对金额。qlib 的风险统计函数把这两种口径做成了一个显式的开关,注释里直接写明了为什么:用累加而不是连乘,是为了避免累计曲线被指数级地扭曲。它甚至在文档里承认,这样算出来的「年化收益」和年化收益的标准定义不一样,是有意为之。

两种口径下,最大回撤的算法也跟着变:

if mode == "sum":
    max_drawdown = (r.cumsum() - r.cumsum().cummax()).min()
elif mode == "product":
    cumulative_curve = (1 + r).cumprod()
    max_drawdown = (cumulative_curve / cumulative_curve.cummax() - 1).min()

上面那行算的是绝对差(跌了多少个百分点的累计值),下面那行算的是相对比例(从最高点跌下来百分之多少)。同一段历史,两个数字,都叫「最大回撤」。你看到一个回撤数字却不知道它是哪一种,那这个数字对你就没有意义。

含不含成本。 一份认真的报告会同时画两条线:一条是不扣交易成本的,一条是扣掉的。qlib 的持仓报告模块就是这么组织的,累计收益和累计超额收益都各画两条,含费与不含费并排放。两条线之间的缝隙有多宽,直接告诉你这套做法有多依赖低成本环境。缝隙宽到一定程度,不含费那条线就是个纯粹的数学玩具。成本假设本身怎么定,是另一个大话题,交易成本假设那篇专门讲。

有没有减掉基准。 一条向上的曲线,可能只是因为这几年大盘本身就在涨。所以专业报告会再画一组「超额」曲线——把同期基准的收益扣掉之后剩下的部分。这条线经常长得跟原来那条完全不像:原图一路向上,扣掉基准后可能是平的甚至向下。基准与超额那篇讲了为什么这一步不能省。

年化因子是个可配置的常数。 把日频收益换算成年化,要乘一个「一年有多少个交易日」的系数。这个系数在不同实现里取值不同(qlib 日频当前默认取 238,且可以通过参数直接覆盖)。同一份日收益,换算系数不一样,报出来的年化数字就不一样。所以年化数字之间的横向比较,只有在口径一致时才成立。

一句话:看到一条曲线,先看纵轴,再看形状,最后才看终点。 顺序反了,后面全是白看。

第二步:看斜率是不是稳定

现在开始看形状本身。第一个要问的是:这条线的爬升是均匀的,还是集中在某几段。

肉眼就能做初步判断——把曲线在时间轴上大致分成四段,看每一段各自贡献了多少。如果四段各贡献四分之一左右,这是一条斜率稳定的曲线;如果三段几乎是平的,第四段陡然拉起,那你实际上看到的不是一套持续有效的方法,而是一次成功的下注加上很长的等待。

这个区别为什么要命?因为「持续有效」可以外推,「一次成功」不能。前者你可以合理期待它继续;后者你只知道过去发生过一次,不知道下一次在什么时候,甚至不知道还有没有下一次。

再往下一层:斜率变化的时点在哪里。如果曲线的陡峭段全部落在某一类市场环境里(比如某段单边行情),那你手上这套东西大概率是那类环境的专用工具,环境一换就失灵。曲线上的每一个拐点都值得你去查一遍当时市场发生了什么,这件事比多算三个统计量有用得多。

第三步:回撤要看形状,不是看那一个数字

大多数报告只给你一个数字:最大回撤。但「最大回撤 20%」这句话里,丢掉了三个更重要的信息——它发生在什么时候、持续了多久、这样的坑总共有几个

正经的做法是把回撤单独画成一条线。算法本身简单到几乎不需要解释:

max_drawdown_df["cumulative"] = profit_results[value_col].cumsum()
max_drawdown_df["high_value"] = np.maximum(0, max_drawdown_df["cumulative"].cummax())
max_drawdown_df["drawdown"] = max_drawdown_df["cumulative"] - max_drawdown_df["high_value"]

cummax 是「到目前为止的历史最高点」。当前值减去历史最高,得到的永远是零或负数——这条线只会在水面下方。freqtrade 把这个东西叫 underwater(水下)曲线,名字起得很传神:只要你还没创新高,你就一直在水里,这条线告诉你水有多深、你在水里待了多久。

把这条水下曲线和上面那条收益曲线并排放,很多东西一眼就清楚了:

  • 坑的深度:最深的那一处,就是最大回撤。
  • 坑的宽度:从入水到重新浮出水面的那段横向距离,比深度更折磨人。一个 15% 但只花两周填平的坑,和一个 15% 但趴了十八个月的坑,是完全不同的两件事。这一层单独展开在回撤恢复期
  • 坑的个数与分布:三个中等的坑均匀分布,和一个巨坑压在最近半年,含义天差地别。后者意味着这套东西可能正在失效,而不是「历史上曾经有过一次考验」。

还有一个细节值得注意:最大回撤的起止日期怎么定位。先找到跌得最狠的那个谷底,再从谷底往前回溯,找到它之前的那个最高点——顺序是先定谷再定峰,不是先定峰。qlib 的报告模块和 Vibe-Trading 的风险透视模块用的是同一套逻辑,前者还会把这个区间在图上画成一块阴影矩形,让你一眼看到「这段时间你在受苦」。回撤数字本身怎么理解、多深算深,见最大回撤

第四步:收益是不是靠少数几天

这是最能揭穿一条漂亮曲线的检查,也是最少人做的。

问题很朴素:如果把赚得最多的那几天去掉,这条曲线还剩下什么? 如果去掉三天就从盈利变成亏损,那你手上的不是一套方法,是三次运气。

真实系统里,这件事被拆成了好几个具体的统计口径。freqtrade 的日度统计会把每天的盈亏汇总出来,然后数三个数:赚钱的天数、打平的天数、亏钱的天数,再加上最好的一天和最差的一天分别赚了多少亏了多少。这几个数放在一起就很说明问题:一套「胜天数远多于亏天数、但最差一天吞掉半年利润」的东西,和一套「胜率不高但赢的时候赢很多」的东西,是两种完全不同的风险结构,需要的心理承受力也完全不同。

它还提供按周期拆分的视图——按星期几拆,按周拆,按月拆。按星期几拆听起来有点玄学,但用途很实在:如果盈利极度集中在某一个工作日,那大概率不是市场规律,而是你的数据或者结算逻辑在那一天有特殊处理。

再往上是几个把「集中度」压成单个数字的指标:

average_win = (profit_sum / nb_win_trades) if nb_win_trades > 0 else 0
average_loss = (loss_sum / nb_loss_trades) if nb_loss_trades > 0 else 0
expectancy = (winrate * average_win) - (loserate * average_loss)

这是期望值:胜率乘平均盈利,减去败率乘平均亏损。它回答的是「平均每做一笔,指望赚多少」。胜率高不等于期望值高——赢十次每次赚一块、输一次亏二十块,胜率超过九成,期望值是负的。

同一份代码里还有两个更严格的:

sqn = math.sqrt(number_of_trades) * (average_profits / profits_std)
...
_, p_value = stats.ttest_1samp(returns, popmean=0)

上面那个把「平均每笔赚多少」除以「每笔之间的波动有多大」,再乘上笔数的平方根——笔数越多、结果越稳定,这个数才越高。它的用意就是防止你被十笔交易的漂亮成绩说服。下面那个更直白,是一个统计检验:假设这套东西其实一分钱不赚(每笔期望为零),那么观察到你手上这份成绩的概率有多大。概率很大,说明你看到的完全可能只是噪音。

这里有个必须说清楚的边界:这类检验只是把「样本量够不够」这件事量化了,它不能证明一套方法有效。它假设每笔交易之间相互独立、分布稳定,而真实市场里这两条经常都不成立。把它当成一个「样本量警报器」是合适的,当成结论就走远了。想系统理解「这个成绩会不会只是运气」,本库里讲蒙特卡洛检验的那篇给的是另一条思路。

第五步:找那些不该出现的台阶

前面四步都在看「表现如何」。这一步不一样——它是在用曲线的形状反查数据和代码有没有毛病

完美的直线是可疑的。 市场收益是有波动的,一条几乎没有毛刺、以恒定角度上升的线,通常意味着某个环节把波动抹掉了。常见来源包括:收益是按某个固定假设推算而非按实际价格结算的、成本被当成常数扣掉了、或者中间有一大段数据缺失被平滑填充了。

垂直的跳变要查来源。 曲线上一段完全水平然后突然垂直跳一大截,第一反应不该是「这里抓到一波大行情」,而是「这里的价格序列是不是断了」。真实系统里制造这种形状的机制有好几个,而且大多数是完全合理的记账约定,不是 bug:

一个是收益只在平仓那一刻入账。freqtrade 把交易明细汇总成累计收益曲线时,是按平仓时间把每笔盈亏加总,再把中间没有平仓的时刻向前填充:

_trades_sum = trades.resample(timeframe_freq, on="close_date")[["profit_abs"]].sum()
df.loc[:, col_name] = _trades_sum["profit_abs"].cumsum()
df.loc[df.iloc[0].name, col_name] = 0
df[col_name] = df[col_name].ffill()

ffill 就是向前填充。这意味着一笔持有三个月的仓位,在它持有期间曲线是平的,浮盈浮亏一点都看不见,直到平仓那天一次性跳上去。这条曲线的形状因此天然带台阶,台阶的宽度等于持仓时间。理解了这一点,你就不会把「阶梯状」当成异常了——但你也得知道,这条线不反映持仓期间你账户的真实波动,你在持有期间经历的心理压力,图上一点痕迹都没有。

另一个是停复牌导致的整段涨跌被记到一根 K 线上。计算逐根收益时,上一根的价格通常要向前填充再错位一格,Vibe-Trading 的收益计算函数在注释里把这个约定的后果写得很清楚:这样做的效果是把跨越缺口的整段涨跌,全部归到复牌后的那一根上——对于一个真的持有穿过了停牌期的仓位来说,这确实就是它赚到或亏掉的钱;但如果你拿这个序列去算相关性之类的统计量,这一根就是个异常值,该丢掉而不是留着。同一个数据处理,用在收益核算上是对的,用在统计上是错的。

同一段注释还提到另一件事:当上一根价格接近零甚至为零时,普通的百分比变化会算出天文数字,累计连乘之后整条曲线直接爆掉。它给的复现例子里,同一段行情用连乘算出来是暴涨,用首尾价格比直接算是大幅亏损——两个结论完全相反,全因为中间有一个接近零的价格。曲线尾部那个不合常理的暴涨,很可能只是一次除以接近零的除法。

🚧 避坑

看到超出常识的形状,先怀疑数据,再怀疑天才。 一条在最后三个月突然指数级拉升的曲线,可能是抓住了一波大行情,也可能是某个价格字段出了零值、某段停牌被填充成了跳空、或者收益口径在中途换了。这两种可能性里,后者出现的频率高得多。分辨方法很朴素:把那一段的原始价格和成交记录调出来,一笔一笔对。图上看着最激动人心的那一段,往往是最该被逐笔核对的那一段。

系统层:一份认真的报告为什么要画七行

把上面五步合起来,就能理解为什么专业工具从不只画一条线。

qlib 的持仓分析报告,默认输出的是一张纵向叠了七行的图。这七行分别是:基准的累计收益、含费与不含费的累计收益(同一行两条线)、不含费的回撤水下曲线、含费的回撤水下曲线、累计超额收益(同样含费不含费两条)、换手率,以及超额收益各自的水下曲线。最大回撤那段区间还会被画成一块贯穿上下的阴影矩形。

这个布局本身就是一份检查清单:

  • 把基准和自己画在同一行 → 逼你回答「这些涨幅有多少是大盘给的」
  • 含费不含费并排 → 逼你看见成本吃掉了多少
  • 回撤单独占一行还画成填充面积 → 逼你看坑的宽度而不只是深度
  • 超额收益和超额回撤各占一行 → 扣掉大盘之后,那条线还站得住吗
  • 换手率单独占一行 → 这一行最容易被忽略。一条漂亮的曲线如果建立在极高的换手上,那它对成本假设的敏感度就极高,成本估计稍微悲观一点,整条线可能就翻过去了。

它还有个小细节:作图前会在最前面插一个值为零的时间点,标记成 T0。也就是强制所有线从零开始。这不是美观问题——起点不统一的两条曲线,视觉上的高低对比是骗人的。

你不需要复制这套图。但下次看到只有一条线的收益曲线时,你应该本能地想到:另外六行去哪了。

失效边界与常见误用

拿两条口径不同的曲线比高低。 一条含费、一条不含费;一条累加、一条复利;一条减了基准、一条没减。放在一起比,比的是口径不是能力。

用曲线好看反推方法正确。 曲线是结果,不是证据。一条经过参数反复调整才变好看的曲线,好看本身就是被调出来的。这一层属于前视偏差过拟合的地盘,光看图看不出来。

回测曲线当成账户曲线。 前面说了,按平仓入账的曲线在持仓期间是平的,它省略了你实际会经历的浮亏。真实账户的曲线要毛糙得多、难受得多。

只看曲线不看笔数。 三十笔交易画出来的曲线和三千笔画出来的,视觉上可能一样漂亮,可信度差着量级。看图之前先看样本量。

忽略时间跨度。 一条只覆盖单一市场环境的曲线,无论多漂亮,都只证明了它在那种环境下能活。跨越过牛熊、跨越过流动性紧张时期的曲线,才有资格谈稳健。

小结

  • 看一条收益曲线的正确顺序是:先问纵轴是什么口径(累加还是复利、含不含成本、减没减基准、年化系数取多少),再看形状,最后才看终点数字。
  • 斜率告诉你收益是持续来的还是一次性来的;回撤要画成水下曲线看形状——深度、宽度、个数与分布,宽度往往比深度更难熬。
  • 把最赚钱的几天去掉再看一遍:胜负天数分布、期望值、按笔数开方折算的稳定性、以及「假设一分不赚能不能碰巧看到这份成绩」的统计检验,都在回答同一个问题——这是方法还是运气。
  • 不该出现的形状本身就是线索:完美直线、突然的垂直跳变、尾部的指数级拉升,先查数据(停复牌填充、零价格、口径切换、按平仓入账的记账约定),再考虑天才的可能。
  • 专业报告画七行不是为了好看,是七个你必须回答的问题;只给你一条线的图,默认缺了六行。

一句话记住这篇:别问它最后赚了多少,问它是怎么走过来的。

本文所引源码与注释均来自上述快照版本,是阅读代码得出的机制说明,不是运行结果;具体行为请以你手上那个版本为准。回 量化与 AI 投研概念库 看其它概念各自在说什么;本站内容为投资教育,不构成任何投资建议,边界见 免责声明

内容有错、看不懂、或想看下一篇?告诉我们 →

本文为投资教育整理,不构成任何投资建议;不荐个股/基金、不预测点位、不承诺收益。关键数据与结论请结合权威来源自行验证,并见风险免责声明