目标函数设计:你优化的那个指标,正悄悄替你选好了策略
"回测无幻觉"系列文章。
📄 本文成长为了一篇研究论文。 以下每一个数字都来自同一个确定性脚本,它构建了受控的真实基准——一个在中等信号区间内带有已知优势、且处处充满肥尾噪声的合成市场——然后在六种不同的目标函数下运行同一次阈值搜索,并在样本外测量每种目标函数实际选出的是哪个策略。可在线阅读论文(交互版 + PDF):objective-design.marketmaker.cc,代码与数据见 github.com/suenot/objective-design-degeneracy。
你想要那个最好的策略。于是你跑一次搜索——扫一遍阈值、回看窗口、止损距离,留下得分最高的那个设置。搜索跑完,交给你一个赢家。合情合理。标准做法。这是地球上每一个优化器、网格搜索、超参数调优器都在做的事。
但看看这个动词:"得分最高"。在什么上最高?在搜索能给任何东西加冕之前,你必须先交给它一个要最大化的单一数字——一个目标函数。PnL。夏普比率。只算你交易过的那些 K 线上的夏普比率。收益除以最大回撤。你敲下了其中一个,多半没怎么细想,然后搜索花了上百万次评估,精确地做了你要求的事。
这一个选择不是走个形式。它就是整个决定。搜索找不到"一个好策略"——抽象意义上根本没有这种东西。它找到的是让你选中的那个标量最大化的策略,而不同的标量,在同一份数据上会指向截然不同的策略。目标函数是握着方向盘的那只暗手,而多数时候没人在看它。
这整篇文章可以浓缩进一张表。一次阈值搜索,一个带有真实已知优势的合成市场,六种目标函数——以及它们各自选出的六种策略,用留出数据测量:
| 目标函数(搜索最大化的对象) | 平均市场曝光率 | 样本内夏普比率 | 样本外夏普比率 | 退化赢家占比 |
|---|---|---|---|---|
| 原始 PnL | 0.859 | 1.76 | 1.61 | 0% |
| 全时间线夏普比率 | 0.740 | 1.82 | 1.71 | 0% |
| 逐笔("活跃")夏普比率 | 0.286 | 1.00 | 0.70 | 57% |
| 曝光率下限() | 0.740 | 1.82 | 1.71 | 0% |
| 交易笔数收缩(conf_k) | 0.523 | 1.54 | 1.31 | 20.7% |
| 稳健目标函数(下限 + conf_k) | 0.675 | 1.78 | 1.70 | 0.2% |
600 个独立种子,每个 根 K 线,每次搜索 80 个候选阈值,样本内与样本外独立抽取。年化夏普比率(每年 252 个周期)。"退化"指所选赢家在市场中的时间占比低于 5%,或样本外夏普比率非正。这个市场的真实最优值,是样本外年化夏普比率 1.77。
把第三行盯着看,直到它刺痛你为止。逐笔夏普比率——代表着一整个"活动条件"指标家族(逐笔夏普比率、期望值、van Tharp 的 SQN、胜率),全都只在你交易过的那些 K 线上计算——选出的策略,样本外表现比另外一半的目标函数都要差,而且57% 的时候都退化得一塌糊涂。这不是一个略逊一筹的目标函数。在这份数据上,它是一个陷阱,搜索超过一半的时候都会一头栽进去。再看看它上面那一行:朴素的全时间线夏普比率从不退化,样本外得分 1.71。这就是整个修复过程的结论,提前剧透——诚实的修复办法很简单,就是在整条时间线上测量;下面那些更花哨的补丁方案,最好的情况下也只是追平这个数字,从未超过它。本文剖析的就是这个陷阱及其修复办法,全程都有已知的真实基准撑腰,所以"目标函数是否选对了策略"是一个事实,而不是一个主观意见。
第 1 幕——秘密的决定:古德哈特定律,就是搜索本身

1975 年,经济学家 Charles Goodhart 写下了一句比他其余所有工作都更长寿的话:
"任何被观测到的统计规律,一旦被用于控制目的而承受压力,都倾向于崩溃。"
更精炼的通俗版本——通常被归功于 Marilyn Strathern——是:一旦一个衡量指标变成了目标,它就不再是一个好的衡量指标。
参数搜索是古德哈特定律最纯粹的实例。目标函数就是那个衡量指标。搜索就是那个压力——成千上万、上百万次尝试,把这个指标往它能到达的最高处推。而搜索完全不在乎你想表达的是什么。它只在乎那个数字。如果存在任何一种能让数字变大、却和真实的、可交易的优势毫无关系的办法——很少交易、大部分时间空仓、逮住几个幸运的离群值——搜索都会找到这条路,因为找到最大值正是它被造出来要做的唯一一件事。
这和 AI 安全文献里所说的**奖励黑客(reward hacking)**是同一种失败:一个针对你想要之物的代理指标进行优化的智能体,会利用代理指标和真实目标之间的每一处缝隙。你的搜索就是那个智能体。"夏普比率"就是那个代理指标。"一个我能放心用真金白银在下个季度交易的策略"才是真正的目标。它们之间的缝隙,正是这整个学科安身立命之处。
要亲眼看着这道缝隙裂开,我们需要一个我们知道真相的世界。于是我们造了一个。
市场。 每个周期,一个预测因子 (一个标准正态信号)先出现,随后是它部分预测到的收益 。优势是真实存在的,但有边界——它只存在于一个中等信号区间 之内,区间之外就消失了:
有两个设计选择很关键。第一,优势活在中等区间内:极端信号不携带任何预测信息,所以策略应该交易中段、跳过尾部。第二,噪声 是肥尾的(自由度为 4 的 Student- 分布,正是真实收益实际具有的那种厚尾)——但这个成分放在这里是为了贴近现实,而不是构成机制。很容易就会想说,正是肥尾让这个陷阱得以成立,我们一开始也正是这么假设的,直到我们跑了对照实验:这个市场的高斯噪声版本(结果中的 gaussian_control,300 个种子)几乎原样重现了这个陷阱——高斯噪声下逐笔目标函数依然在 55.7% 的时候退化,肥尾情形下是 57.0%,其样本外夏普比率分别是 0.71 对 0.70。所以这个陷阱跟肥尾无关。它是一种纯粹的小样本加选择效应:在约 20 个低曝光阈值上,取一个只在少量观测上算出的夏普比率的最大值,总会有某个幸运的角落看起来惊艳绝伦。任何噪声分布都会产生这种效应;一个只在场内待了几根 K 线的策略,纯靠运气就能撑过几次有利的行情,报出一个毫无意义的样本内数字。我们保留肥尾是因为真实收益确实有肥尾,不是因为这个陷阱需要它。
策略。 一个单参数族。只要信号幅度低于阈值 ,就顺着信号方向交易,否则空仓:
一个极小的 只在最小、最不起眼的信号上交易——一张几乎从不入场的稀有交易彩票。一个接近区间边缘的 能捕获全部真实优势,曝光也充分。一个巨大的 什么都交易,包括那些不携带优势、只会添噪的区间外 K 线。
def gen_dataset(T, rng, beta=0.30, band=1.0, tail_df=4):
s = rng.standard_normal(T)
edge = beta * np.where(np.abs(s) <= band, s, 0.0) # edge ONLY inside |s| <= 1
t = rng.standard_t(tail_df, T) / np.sqrt(tail_df / (tail_df - 2.0)) # fat tails, unit var
return s, edge + t
def simulate(s, r, theta):
pos = np.where(np.abs(s) <= theta, np.sign(s), 0.0) # trade the band, skip outliers
strat = pos * r
active = pos != 0.0
exposure = active.mean() # fraction of bars in a position
sharpe_full = strat.mean() / strat.std(ddof=1) # on the WHOLE timeline
sharpe_active = strat[active].mean() / strat[active].std(ddof=1) # on ONLY the active bars
return dict(exposure=exposure, n_trades=int(active.sum()),
sharpe_full=sharpe_full, sharpe_active=sharpe_active, pnl=strat.sum())
因为这个市场是我们自己造的,我们可以直接算出真相——在全部 600 个种子上,每个阈值的样本外平均表现。真实最优值落在 :正好在信号区间的边缘,大约 70% 的时间在场内(推算:一个标准正态信号落在 内的概率为 ),样本外年化夏普比率为 1.77。这就是每一个目标函数都在努力寻找的数字。把它记在心里:θ≈1.04,样本外夏普比率 1.77,约 70% 的时间线处于场内。 任何目标函数选出的结果,只要远离这个点,就是目标函数出了问题,而不是市场太难。
第 2 幕——陷阱:八笔幸运的交易,夏普比率 21,一场海市蜃楼

现在把一个朴素目标函数,放到这个市场的一次具体抽取上——种子 6。关于这个种子的坦白:它不是第一次抽取,也不是随机选的。我们扫描了各个种子,找出一个逐笔赢家退化得格外触目惊心的,专门挑了这一个,好让这个机制无所遁形。它展示的结果是彻头彻尾典型的——第 3 幕会确认,逐笔目标函数在全部种子中有 56% 会选出曝光率低于 5% 的彩票——但种子 6 的量级落在这个分布的极端一端。请把它读作一种常见失败的一个格外触目惊心的实例,而不是一个中位数实例。我们优化的是逐笔夏普比率:只在策略实际持仓的那些 K 线上计算的夏普比率。这是一件极其自然的事,你很想拿出来汇报——"它一交易,交易质量到底怎么样?"这感觉像是把技巧从闲置中剥离了出来。实际上恰恰相反。
以下是逐笔夏普比率在种子 6 上加冕的策略:
- 阈值 ——只在最微小的信号上交易。
- 市场曝光率 0.4%——99.6% 的时间空仓。
- 八笔交易。 八笔。在 2000 根 K 线里。
- 样本内逐笔年化夏普比率:21.09。
- 样本内全时间线夏普比率:0.82。
- 样本外全时间线夏普比率:0.13。
逐笔指标读数是 21.09——一个从没有真实策略报出过的数字,一个能让一支基金就此发行的数字。而它完全是一场海市蜃楼。那八笔交易碰巧逮住了几次有利的行情;只在这八根 K 线上测量,均值与标准差之比高得离谱。但在整条时间线上——策略在 99.6% 的时间里空仓——那份"优势"基本没贡献任何东西:样本内全时间线夏普比率只有 0.82,在新数据上崩塌到 0.13。目标函数选出的这个赢家,在任何交易意义上,都等同于空仓。
而且在那个阈值上,甚至根本不存在真实的优势。回想一下这个市场:优势活在区间 之内,而 正处在信号最弱的死中心。 处的真实样本外曲线是 −0.01——和零没有区别(从真实基准曲线推算得出)。搜索找到的不是一个微小的真实优势。它找到的是八次幸运的噪声抽取,然后把它们汇报成了一个夏普比率 21。
这就是这个陷阱的缩影:逐笔夏普比率会奖励策略尽可能少地交易,因为你站立的 K 线越少,其中几根走运的概率就越高,而这个指标从来不会问一句"但你真的在场内吗?"种子 6 的量级是精心挑选出来的——我们就是在找一个触目惊心的例子——但它的类型不是精心挑选的。在全部 600 个种子中,逐笔夏普比率有 57% 的时候选出一个退化的赢家(几乎不交易,或者样本外亏损),其中具体有 56% 是曝光率低于 5% 的彩票。典型的退化选择比种子 6 的夏普比率 21 温和得多:在全部 600 个种子上取平均,逐笔目标函数选出的赢家,样本内逐笔夏普比率为 4.58,平均曝光率 0.286——大部分时间依然空仓,只是没有 99.6% 那么极端。种子 6 把这个机制戏剧化地呈现了出来;真正该让你担心的是那个 56%。超过一半的时候,这个稀松平常的指标塞给你一张彩票,还管它叫策略。
第 3 幕——统计学真相:六种目标函数,600 个种子

单个种子什么也证明不了;它只是一个例证。要衡量一个目标函数,我们必须问它平均而言在许多独立市场上选出的是什么,然后用搜索从未见过的数据去给这个选择打分。于是:600 个种子,每个都是这个市场的一次独立抽取;对每一个种子,在每种目标函数下跑一次 80 阈值搜索;记录它选出的结果的曝光率、样本内与样本外夏普比率,以及这次选择是否退化。
| 目标函数 | 平均曝光率 | 样本内夏普比率 | 样本外夏普比率 | 样本内→样本外跌幅(绝对值) | 退化占比 |
|---|---|---|---|---|---|
| 原始 PnL | 0.859 | 1.76 | 1.61 | 0.15 | 0.0% |
| 全时间线夏普比率 | 0.740 | 1.82 | 1.71 | 0.11 | 0.0% |
| 逐笔夏普比率 | 0.286 | 1.00 | 0.70 | 0.30 | 57% |
| 曝光率下限() | 0.740 | 1.82 | 1.71 | 0.11 | 0.0% |
| conf_k 收缩() | 0.523 | 1.54 | 1.31 | 0.23 | 20.7% |
| 稳健目标函数(下限 + conf_k) | 0.675 | 1.78 | 1.70 | 0.08 | 0.2% |
"样本内→样本外跌幅"这一列,是年化夏普比率从样本内到样本外的绝对下降幅度(例如 就是下降 0.30),不是百分比。另外注意"曝光率下限"这一行和"全时间线夏普比率"逐字节完全相同:这不是巧合,第 5 幕会解释原因。
三个事实跳了出来,每一个都是一堂课。
逐笔夏普比率是唯一会退化的朴素目标函数。 它的平均曝光率是 0.286——它选出的策略大部分时间都在空仓——它的样本内夏普比率 1.00 下降了 0.30,跌到样本外的 0.70,是全场表现最差的。注意这个破绽:它的样本内数字(1.00)本身甚至并不亮眼,但在任何单个种子上,它都乐意报出一个 21 的逐笔数字。均值之所以被抹平,是因为幸运窗口指向的方向是随机的;能撑到样本外的只有 0.70,而 57% 的具体选择彻头彻尾就是垃圾。
曝光感知型目标函数天生安全。 原始 PnL 和全时间线夏普比率从不退化(0.0%)。原因是结构性的:两者都在整条时间线上测量,所以一个 99.6% 的时间都空仓的策略,在它们眼里几乎赚不到任何东西。你没法靠很少交易来薅一个全时间线指标的羊毛——空仓会被直接自动惩罚,因为空仓的 K 线也计入分母。这是全文最重要的一个想法,第 6 幕会回到这一点。
原始 PnL 安全,但不是最优——它曝光过度。 仔细看:原始 PnL 的平均曝光率是 0.859,全场最高,它的样本外夏普比率(1.61)比全时间线夏普比率(1.71)和真实最优值(1.77)都要低那么一截。PnL 奖励的是待在场内,所以搜索把 推得太高了(在种子 6 上,原始 PnL 选出 ,而最优值是 1.04),拖进了一堆不携带优势、只会添噪的区间外 K 线。它没有彻底崩盘——但它偏离真实最优值的方向,和逐笔陷阱正好相反。不同的目标函数,不同的偏差,同一个教训:是这个指标选中了策略。
还有两行我们尚未讨论——曝光率下限和 conf_k——它们就是修复方案。这是下一幕的内容。
第 4 幕——为什么八笔交易永远不可信

在修复这个陷阱之前,值得精确地弄清楚为什么八笔交易会产生一个毫无意义的夏普比率 21——因为修复办法正是直接从这个原因推出来的。
夏普比率是一个估计值,而估计值都带着误差棒。Andrew Lo 在 2002 年给出的结果,在最宽厚的假设下(独立同分布的高斯收益),给出了用 个观测值估计出的夏普比率的标准误:
误差只以 的速度收缩。把这个陷阱代进去。种子 6 上的逐笔夏普比率年化是 ,折算到单次观测是 ,是在 根 K 线上算出来的。标准误是
(由 Lo 的公式推算而来)。点估计是 ;它的一倍标准差误差棒量级是 ——请把这读作一个示意性的数量级,而不是一个校准过的置信区间,因为这个公式假设的是独立同分布高斯收益,而我们的肥尾 噪声违反了这个假设。即便如此,信息也是明确无误的:"夏普比率 21"这个数字,来自一个宽到基本不携带任何信息的分布——而这还是宽厚的算法,因为 Mertens 的扩展表明肥尾和偏度只会进一步放大标准误。一次稀有交易回测的夏普比率,在每一个方向上都比它的点值更不可信:观测太少,分布也不对。
这正是**最短业绩记录长度(Minimum Track Record Length)**要形式化表达的东西(Bailey & López de Prado,2012)。它把问题倒过来问——我需要多少个观测值,才有资格在置信度 下相信这么大的一个夏普比率?——
把"少交易的回测要少信一点"变成了一个明确的、可核查的交易笔数。对目标函数设计而言,深层的要点在于:一个好的目标函数应该从内部就强制执行一个最短业绩记录,而不是留给人在事后才注意到,赢家竟然只建立在八个观测值之上。逐笔夏普比率做的恰恰相反——它正是靠把观测数量压向最小值来被最大化的。任何一个最优点落在"交易越少越好"上的目标函数,从构造上讲,都是一个在寻找自己最不可靠估计的目标函数。
这个陷阱里有两种失败叠加在一起,把它们都点名出来,就能告诉我们怎么修。第一,小样本噪声:八个观测值钉不住任何比率。第二,选择:那八根 K 线不是白白落到我们手里的——搜索选中了落在它们身上的那个阈值,部分正是因为它们运气好。搜索是一个最大化器;它总能找到空间中噪声碰巧看起来像信号的那个角落。你没法靠一个更好的点估计来耍小聪明绕过这一点。你必须改变"最好"的含义,让那个幸运的角落不再是最大值。
第 5 幕——修复:一道曝光率下限和一次交易笔数收缩

我们已经给两种病命了名——交易太少和建立在观测太少之上——所以我们开出两剂药方,各自对准一种病。
药方 1:曝光率下限。 最简单的可能修复方案。直接拒绝任何在场内时间占比不到 的策略——如果你几乎不交易,你的得分就是 ,搜索没法选中你。但给什么东西设下限这件事本身有一处诚实的微妙之处,也是这整篇文章里一个不动声色的教训。作为一个独立的目标函数,我们给全时间线夏普比率设了下限,而在这个市场上,这完全没有改变任何东西:全时间线夏普比率本身选出的赢家曝光率已经在 74% 左右,所以 20% 的下限从来没有真正生效过。这正是为什么上面表格里"曝光率下限"和"全时间线夏普比率"两行逐字节完全相同——把一道下限拧到一个本来就安全的指标上,你不过是重新推导出了全时间线夏普比率而已。下限只有在守护一个否则会一路冲向角落的指标时,才会做出看得见的工作:也就是逐笔指标,正如下面的稳健目标函数那样。换句话说,在这份数据上,"要求曝光"和"在整条时间线上测量",是同一种干预手段的两个名字。
药方 2:交易笔数收缩——"conf_k"。 用于你被迫使用逐笔指标、又想要一种柔和修正而不是硬性截断的场合:根据夏普比率建立在多少笔交易之上,连续地给它打折。乘上 ,其中 是交易笔数, 是一个固定的"置信常数"——一个在搜索之前就选定的、以交易笔数为单位的先验强度:
当 时,无论原始夏普比率有多大,得分都会被压碎到零;当 时,得分收敛到原始夏普比率。这和 MinTRL 以及第 4 幕里的标准误,是同一种修正逻辑——把一个小样本估计值,按其样本量的递减函数收缩向零——只不过这次是直接折进目标函数内部,而不是作为事后过滤器施加。最接近的有名字的先例是 van Tharp 的系统质量数(System Quality Number)(),它同样让一个逐笔质量指标随交易笔数 缩放——尽管函数形式不同( 无界增长,而 会在 1 处饱和)。在形态上,我们这个更像是一种贝叶斯精度加权 / 经验贝叶斯风格的收缩;它是我们为这个问题构造出来的东西,不是从文献里搬来的一个有名字的估计量。
def obj_active_sharpe(m): # the trap: Sharpe on only the active bars
return m["sharpe_active"]
def _shrink(n, conf_k): # trade-count shrinkage n / (n + k)
return n / (n + conf_k) if (n + conf_k) > 0 else 0.0
def obj_confk(m, conf_k=40.0): # few trades -> little credit
return m["sharpe_active"] * _shrink(m["n_trades"], conf_k)
def obj_robust(m, e_min=0.20, conf_k=40.0): # both cures at once
if m["exposure"] < e_min: # floor: reject strategies that barely trade
return -np.inf
return m["sharpe_active"] * _shrink(m["n_trades"], conf_k)
现在到了诚实的部分:多少下限,多少收缩?把两者都扫一遍,通读整个曲面。每个格子都是 200 个种子(600 个的三分之一子集,为了让这个二维扫描的成本可控)上的平均样本外夏普比率,旁边附上退化率:
| \ conf_k | |||
|---|---|---|---|
| 0.00 | 0.66 (59.5%) | 1.26 (22.5%) | 1.47 (11.5%) |
| 0.05 | 1.43 (10.0%) | 1.53 (6.0%) | 1.60 (4.0%) |
| 0.10 | 1.64 (1.5%) | 1.65 (1.0%) | 1.67 (1.0%) |
| 0.20 | 1.71 (0.0%) | 1.71 (0.0%) | 1.71 (0.0%) |
| 0.35 | 1.73 (0.0%) | 1.73 (0.0%) | 1.73 (0.0%) |
平均样本外年化夏普比率,括号内是退化率。左上角格子 就是原始逐笔夏普比率——没有下限,没有收缩:样本外 0.66,退化 59.5%。这和第 3 幕逐笔那一行的目标函数是同一个,那里读数是 0.70 / 57%;这一点小差距纯粹来自种子集——这次扫描用了 200 个种子,蒙特卡洛用的是全部 600 个。同一个指标,更小的样本。
这个曲面用三个读数讲了一个干净利落的故事。
每一剂药单独都有效。 沿着顶行向右移动(加收缩,不加下限):样本外从 一路攀升,退化率从 一路下降。沿着左列向下移动(加下限,不加收缩):样本外从 攀升,退化率从 下降。任何一个旋钮,单独拧动,都能独立地提升样本外表现、消灭退化。曝光率下限是这里更强的那根单独杠杆,因为它正面攻击的是这个陷阱的定义性特征——接近零的曝光率。
合在一起,它们抵达了那片高原——而那片高原,就是全时间线夏普比率本身。 到 时,无论收缩取哪个水平,这一行都平平地停在样本外 1.71、0% 退化;推到 ,它才微微爬到 1.73。但仔细看看那个 1.71 到底是什么:它正是第 3 幕里朴素全时间线夏普比率在没有任何下限、没有任何收缩的情况下报出的那个精确得分。这些补丁方案,最好的情况下并没有超过全时间线夏普比率——它们只是重新构造出了它。而完全修复后的稳健目标函数甚至都没能完全达到那个高度:在全部 600 个种子上,它落在样本外 1.70,还残留 0.17% 的退化,比全时间线夏普比率的 1.71 / 0% 差了那么一丝——它被这个更简单的指标弱支配着。一个折中的中间设置, 配 ,能达到样本外 1.65、1% 退化——如果逐笔指标是被强加给你的,这个设置会很好用,但它永远不是偏爱逐笔指标的理由。
具体数字是依赖尺度的——真正的结果是那个形状。 能完全修复这个市场的具体数值 、,是针对这个特定数据生成过程调出来的;在一个交易频率和尾部厚度都不同的市场上,那片高原会落在别处。能够推广的不是坐标,而是那个曲面:两个方向上都单调抬升,退化率被压到零,一片停在真相处的高原。你需要靠像上面这样的扫描,找出属于你自己的坐标。
把两剂药方合在一起——稳健目标函数,下限 0.20 加 conf_k 40——回到种子 6。陷阱加冕的是 ,八笔交易,全时间线样本外夏普比率 0.13。稳健目标函数选出的却是 :市场曝光率 0.66,447 笔交易,样本外年化夏普比率 1.77。这个 比真实最优值 低一个网格点,所以它找回的是一个接近最优、曝光充分的阈值,而不是正中靶心——它这个单一种子上的样本外夏普比率(1.77)恰好和总体最优值重合。同样的数据,同样的搜索,同样的 80 个候选阈值。只有"最好"的定义变了,而这个改变,把赢家从一个空仓的八笔交易海市蜃楼,搬到了真实的、曝光充分的优势上——耐人寻味的是,这正是朴素全时间线夏普比率在这个种子上选出的同一个阈值。
这次扫描明确揭示了一个警示:在这个市场上,单独的 conf_k 是不够的。在 、没有下限的情况下,各种子上的退化率仍有 22.5%——具体到种子 6,单独的 conf_k 选出 ,35 笔交易,样本外夏普比率 −0.06。三十五笔交易在 的收缩之下依然存活,剩下的得分足以获胜。补上这最后一道缺口的是曝光率下限,因为它直接瞄准了这个陷阱的真正标志——空仓——而不是把交易笔数当成空仓的代理指标去信任它。
第 6 幕——更深的教训:在整条时间线上测量

从修复方案里退后一步,注意一下究竟是什么把安全的目标函数和陷阱区分开来。不是复杂程度。原始 PnL 和全时间线夏普比率都比逐笔夏普比率更简单,而它们从未退化——600 个种子上 0%——完全没有下限、没有收缩、没有任何调参。
分界线只在于一个单一的属性:这个指标测量的是哪个窗口? 逐笔夏普比率只测量策略选择站立的那些 K 线——一个搜索可以随意收缩的自选窗口。全时间线夏普比率和总 PnL 测量的是整条时间线,包括空仓的 K 线。而你没法靠很少交易就让一个全时间线指标变大,因为你空仓的每一个小时,都是分母里一个不赚钱的小时。曝光率下限和 conf_k,说到底,只是给逐笔指标打补丁、补上全时间线指标天生就自带的曝光感知能力的手段——而扫描已经告诉了我们这种补丁的天花板:最好的情况下,它只能追平全时间线夏普比率(样本外 1.70 对 1.71),永远无法超过它。如果你可以自由选择窗口,就选整条时间线,完全跳过这道补丁。
所以,把这条设计原则平铺直叙地说出来:
设计目标函数,让它无法被稀有的幸运交易薅羊毛。 你手上有三件工具,大致按优先顺序排列:
- 在整条时间线上测量。 这是几乎永远不该偏离的默认选项。全时间线夏普比率和总收益,从构造上就具有曝光感知能力——闲置会被自动惩罚,因为空仓的 K 线也算数。如果你发现自己在汇报一个只在"我们活跃的那些 K 线"上算出来的指标,停下来想一想,搜索一旦获得了自由选择这些 K 线的权力,会拿它做什么。
- 要求曝光。 如果你必须使用一个活动条件指标,就给曝光率设一道下限,让搜索无法选中一个几乎不交易的策略。这是对抗这个特定陷阱最强的一根单独杠杆。
- 按交易笔数收缩。 用 给任何比率打折,让一个建立在少数几个观测值上的夏普比率,只能拿到一个建立在成千上万观测值上的夏普比率的一小部分信用。这是把最短业绩记录长度在目标函数层面强制执行:来自少量观测值的数字是不可靠的(第 4 幕),所以一个诚实的目标函数会把这份不可靠性提前定价进去,而不是指望人事后才发现。
这些做法都不会让搜索变得更聪明。它们做的是让目标变得诚实,这样当搜索做它一贯会做的事——找到最大值——那个最大值就是你真正想要的策略。
诚实说明
三条直白说出来的警示,因为一项受控研究只有点明自己的局限,才配得上它的结论。
- 这个市场是合成的,而且是刻意合成的。 一个标准正态信号,一个被限制在 内的线性优势,肥尾的 Student-()噪声——之所以这样选,是为了受控的真实基准,不是为了贴近真实市场。我们只能通过在一份已知哪个策略才是对的数据上运行一个目标函数,来证明它选错了策略。真实市场是非平稳的、自相关的、会切换状态的。肥尾是我们保留下来的一个贴近现实的成分,但——与一个自然而然的第一直觉相反,也与这篇文章本身更早一版草稿的判断相反——它不是驱动这个陷阱的原因:一个高斯噪声对照实验(300 个种子)退化率是 55.7%,而这里是 57.0%,样本外夏普比率分别是 0.71 对 0.70。这个陷阱是一种小样本加选择的伪影,无论有没有厚尾都会存在。这里交付的是诊断和修复模式,不是一个策略,也不是一个普适常数。
- 修复用的数值是依赖尺度的。 能完全堵上这个陷阱的具体下限 和收缩 ,是针对这个特定数据生成过程拟合出来的——它的交易频率、它的尾部厚度、它的优势大小。换一份数据,那片高原会移动。能够迁移的是那个扫描曲面的形状(两个旋钮上都单调抬升、退化归零、一片停在真相处的高原),以及找到属于你自己坐标的方法:把两者都扫一遍,通读那个曲面,不要照抄这些数字。
- conf_k 是我们自己的构造,不是一个有名字的估计量。 交易笔数收缩 ,是我们为这个问题构造出来的一种贝叶斯精度加权 / 经验贝叶斯风格的装置;它的理论依据扎根于已验证的 Lo/Mertens 标准误结果和 Bailey–López de Prado 的 MinTRL,最接近的有名字的亲戚是 van Tharp 的系统质量数(,函数形式不同),但我们不主张 本身在文献中以某个名字出现过。与它配套的药方——曝光率下限、全时间线测量——是被精确表述出来的标准做法。另外注意一下,这里哪些目标函数本来就是安全的:原始 PnL 和全时间线夏普比率从来不需要修复,因为它们一开始就具有曝光感知能力——安全到给全时间线夏普比率设下限,会退化成全时间线夏普比率本身,它选出的赢家早就轻松越过了任何合理的下限。这个陷阱专门针对的是逐笔 / 活跃夏普比率——而即便是完全修复后的逐笔目标函数,也只能追平全时间线夏普比率(样本外 1.70 对 1.71),永远无法超过它。主要的教训不在于修复;而在于一开始就该在整条时间线上测量。
要点总结
- 目标函数就是那个决定,不是走个形式。 搜索找不到"一个好策略"——它找到的是让你交给它的那个标量最大化的东西,而不同的标量,在完全相同的数据上会选出截然不同的策略。选择目标函数,就是选择策略;下游的一切都只是记账。这就是古德哈特定律:你的指标一旦变成了搜索的目标,搜索就会利用它和你真正想表达之间的每一处缝隙。
- 逐笔夏普比率是一个陷阱。 只在策略交易过的 K 线上测量,它靠尽可能少地交易来被最大化——观测值越少,几次幸运行情把比率吹高就越容易(一个高斯对照实验证实肥尾不是必要条件;这是一种小样本加选择效应)。在 600 个种子中,它有 56% 的时候会选出曝光率低于 5% 的彩票,57% 的时候会退化;典型的退化选择,样本内逐笔夏普比率平均为 4.58。在一个刻意挑出的触目惊心的种子上,它给一个八笔交易、0.4% 曝光率的策略加冕,样本内逐笔夏普比率 21.09,崩塌到全时间线样本外夏普比率 0.13。一个建立在八个观测值上的比率,其年化标准误量级是 ±7.7(第 4 幕)——它从来就不是信息。
- 曝光感知型目标函数天生安全。 原始 PnL 和全时间线夏普比率从未退化(0%),因为它们测量的是整条时间线,闲置会被自动惩罚。你没法靠很少交易薅一个全时间线指标的羊毛。原始 PnL 唯一的毛病是相反方向的偏差——它曝光过度(平均曝光率 0.859,样本外 1.61,对比真实值 1.77),把 推过最优值,只为了多待在场内。
- 修复方案有效——但它们只能把你带回全时间线夏普比率的水平。 曝光率下限和交易笔数(conf_k)收缩,各自都能独立地提升样本外夏普比率、把退化率压向零;合在一起,它们抵达一片高原。但那片高原就是全时间线夏普比率:退化率从 处的 59.5%,降到 时的 0%,样本外夏普比率从 0.66 攀升到 1.71——正是朴素全时间线夏普比率不借助任何帮助就能报出的那个精确数字,而完全修复后的稳健目标函数实际上还差了那么一丝没到(样本外 1.70,退化 0.17%:弱支配关系)。在种子 6 上,稳健目标函数找回了一个接近最优、曝光充分的阈值(,比真实值 低一个网格点;447 笔交易;样本外夏普比率 1.77)。把 conf_k 当成逐笔指标被强加给你时的一个后备方案,而不是测量整条时间线的一种升级。具体坐标是依赖尺度的;曲面的形状才是可以迁移的结果。
- 设计目标函数,让它无法被稀有的幸运交易薅羊毛。 按优先顺序:在整条时间线上测量(默认选项)、要求曝光(最强的单独杠杆)、按交易笔数收缩(目标函数层面的最短业绩记录长度)。这些做法都不会让搜索变得更聪明——它们让目标变得诚实,这样搜索必然会找到的那个最大值,才会是一个你真的会去交易的策略。
参数搜索是一个顺从的精灵。它精确地实现你许下的那个愿望,而不是你心里真正想要的那个——而"最大化这个指标"就是那个愿望。把愿望说成逐笔夏普比率,它就会变出八笔幸运的交易,管它们叫一笔财富。把愿望说成让空仓受到惩罚、少数几笔交易只能赚到少数几份信用,同一个精灵,在同一份数据上,就会把真实的优势交到你手上。你部署的那个策略,早在你敲下目标函数的那一刻就已经被选定了。要有意识地去选它。
完整的实验——合成市场、六种目标函数、600 个种子的蒙特卡洛、以及修复扫描曲面,每一个数字都能从同一个确定性脚本重新生成——都收录在配套论文中,见 objective-design.marketmaker.cc,代码与数据见 github.com/suenot/objective-design-degeneracy。
这是我们其他研究从不同角度打的同一场战争的一条战线。Deflated Sharpe Ratio 给一次多重检验之后的搜索赢家定价——本文问的是目标函数究竟有没有选对策略,DSR 问的则是它选中的策略,能否跑赢单靠运气所能产出的东西。即将发布的回测过拟合概率研究从重采样一侧攻击的是同一种选择偏差,给流程而不是给赢家打分。而前视偏差分类法则记录了另一台伪造夏普比率的大机器——来自未来的泄漏——它通过一种截然不同的机制,产生了一模一样的症状(一份辉煌的回测,一到实盘就死掉)。目标函数设计、去膨胀、过拟合概率、前视偏差:一门学科的四个名字——都是为了不被自己的回测骗过。
Authors
Trading-systems engineer
Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.