Skip to content

你的量化回测,可能把命中率当成了净胜率

作 者:老余捞鱼

原创不易,转载请标明出处及原作者。

技术讨论不构成任何操作建议。
模型评测不等于投资建议。
不荐股、不卖课、不承诺收益。
市场有风险,请合法合规投资。

本文约 3200 字 | 预计阅读 8 分钟,后有3段代码和数据来源,建议先收藏


三个数字先摆在这里:+300.1%、87.86%、-98.4%。它们来自同一份量化策略,第一眼都很有诱惑力,很容易以为这是一套能直接拿去跑的策略。而实际上说的却是三种不同层级的证据,有的缺成本,有的缺损益,有的连样本量都不够。

这也是我想写这篇文章的原因。最近“拿来就能跑”的量化材料很多,图都做得漂亮,但信号什么时候发布、按什么价格成交、成本扣没扣,往往被放在最不显眼的地方。图上的高收益和账户里的收益,中间隔着一整套执行细节。

今天这篇就把材料里的三个策略拆开:一个按月排名的动量组合、一个标普500当天到期期权的区间带,以及一个S-1稀释事件。先看它们各自在赌什么,再看图上的数字能说明到哪一步,最后落到代码和执行细节。

三组数字都来自附件图表,我只做算术核对,不把它们写成自己的收益。

策略图上给出的证据我会怎样称呼它
横截面动量12个月,10正2负最近12个月月度收益
SPX 0-DTE区间593天,521次命中区间命中率87.86%
S-1稀释3个案例,3种窗口事件案例,不是完整回测

这三组数字各自能说明的问题差得很远。动量图只有12个月,区间带只统计命中与落空,稀释图只有3个案例。它们共同回答的是“信号长什么样”,还没有回答“按信号执行能不能赚到钱”。

后面顺着三个问题往下看。信号什么时候能看到,规则怎么落地,图上没给的成本和风险又在哪里。

一、先把发布时间对上

时点数据(point-in-time)可以理解成监控录像。某天上午10点30分屏幕上显示什么,就把那一帧锁住。以后即使知道收盘结果,也不能回头修改当时的画面。当时知道什么,就只准用什么。

很多回测看起来很好,问题就出在这。用今天修订过的成分股回测五年前,等于提前把结局递给策略。用收盘后才公布的字段决定当天开盘,也一样。这个坑很隐蔽,代码通常不会报错。

这些数据的发布时间并不一样,我把四个按日更新、和交易时点有关的数据整理成一张表。时间都是纽约时间,不能简单压成“今天”。

数据发布时间最容易用错的地方
风险状态每日10点10分当天开盘提前使用
SPX区间带每日10点30分当成开盘前已知
动量篮子调仓日16点05分用新名单决定当天收盘
Next-Day Movers每日15点30分当成下一日方向信号

日期相同,不等于同时可见,这就是时点数据要管的事。官网专门提醒过,10点10分才发布的风险状态,不能拿去决定当天开盘。规则若在开盘执行,只能使用此前已经拿到的数据。S-1是事件触发,没有每日固定时刻。它的filing_timestamp放到第四节再说。

我一开始也没管datetime.now(),觉得取当天总不会错。把发布时间排在一起才发现,本机日期和纽约日期可能差一天,周末还会取不到数据。示例里换成ZoneInfo,取不到就停,不拿后一天的数据硬补。

二、动量先比相对排名

横截面动量做的是相对排名,不是挑出最近涨幅最大的那只。它先把流动性足够、期权交易也方便的美股放进同一个池子。接着按过去一段时间的表现排队,只取前10只。某只股票自己涨了多少并不重要,重要的是它有没有比同池的其他股票更强。

组合每月重新排一次,调仓时10只等权,可以先理解成每只占10%。一个月后价格变化,权重会偏离10%。回测如果每月恢复等权,就要把调仓产生的换手和成本算进去。

还有一个容易忽略的执行时点。名单在调仓日16点05分更新,而纽约时间的常规收盘是16点。如果名单要到16点05分才更新,回测就不能用当天16点的收盘价成交。顺序反了。这样算,等于假装自己提前拿到了名单。我在研究规则里会把执行放到下一个可交易时点。

图上从2025年5月到2026年4月,10个月为正、2个月为负。最好+51%,最差-18%。我把12个月收益连乘,累计约+300.1%。我盯着这张图看了半天,我仍没找到成本口径,所以这个结果只能按毛收益理解。下一年还能不能维持,说不好,我也不打算用12个月外推。

它能说明这12个月走得强,不能回答扣完成本还剩多少。图里也没有基准和最大回撤。官网说历史可以追到2017年,这与只展示最近12个月是两回事。

动量指数12个月收益
按附图月度收益重画,未扣成本

三段代码按顺序放进同一个.py文件就能运行。API key从环境变量读,只取研究数据,不连接券商。第一段只做一件事,把最新一期10只篮子取出来。

import os
from datetime import datetime
from zoneinfo import ZoneInfo

import requests

BASE = "https://api.alphanume.com/v1"


def get_rows(path, **params):
    response = requests.get(
        f"{BASE}/{path}",
        params={"api_key": os.environ["ALPHANUME_API_KEY"], **params},
        timeout=30,
    )
    response.raise_for_status()
    return response.json()["data"]


rows = get_rows("quant-galore-momentum-index")
latest = max(row["date"] for row in rows)
basket = sorted(
    (row for row in rows if row["date"] == latest),
    key=lambda row: row["rank"],
)
assert len(basket) == 10, "最新篮子不足10只,先检查数据"
for row in basket:
    print(latest, row["rank"], row["ticker"])

assert len(basket) == 10是我加的。少一只就该停下来查,不该让程序带着半张篮子继续跑。后面两段也沿用这个get_rows,并按rank排序,保证拿到的就是当期名单。

三、87.86%,能当胜率吗?

0-DTE是当天到期的期权。区间带给出一个上界和一个下界,可以把它理解成模型对当天收盘范围的估计。铁鹰组合就是围绕这两条线搭出来的结构:上方一组价差在指数冲高时赔付,下方一组价差在指数跌破时赔付。指数留在中间,两组都收权利金。权利金可以理解成期权的价格,也是这类结构先收到的一笔保费。

这类结构的毛收入上限是权利金,最大亏损由行权价的距离和收到的权利金共同决定。翼宽就是同一侧两个行权价之间的间隔,间隔越大,极端行情下赔得越快。到期损益要拿收到的权利金减去赔付和相关费用,行权价和保证金要记账,滑点也别漏。

现在再看87.86%。先看分母。521加72等于593,521除以593等于87.858%。四舍五入正好87.86%。这个数其实只统计收盘有没有落在区间内,不能直接当成组合扣费后盈利的概率。

落在区间外,不一定整笔亏损;落在区间内,也不代表扣掉成本后一定赚钱。我更倾向把它叫区间命中率,少写“交易”两个字,是在给结论上刹车。

SPX区间命中与落空
521次命中、72次落空,非组合损益

风险状态过滤器提供的也不是涨跌方向。它看的是期权市场对未来波动的定价,不看指数过去涨了还是跌了。1代表高风险状态,0代表相对平静。官网样本覆盖3833个交易日。两种状态年化收益分别为13.0%和12.7%,几乎贴着。

已实现波动率却是24.7%和12.8%,高风险状态约为低风险状态的1.93倍。这组数字说明的是路更颠,不是车一定往下开。拿它调整仓位,常见做法是高风险日把仓位调小,背后的逻辑是波动大时少承担一点风险,并不等于躲开下跌。这条规则本身还得单独回测,我暂时不替它补收益。真实成本下还能剩多少优势,我说不好,所以只把它当辅助信息。

today = datetime.now(ZoneInfo("America/New_York")).date().isoformat()
band = get_rows("spx-0dte-strike-band", date=today)
regime = get_rows("sp500-risk-regime", date=today)

if not band or not regime:
    print(today, "数据尚未发布,保留空记录")
else:
    b, r = band[0], regime[0]
    if b["date"] != r["date"]:
        raise ValueError("两个接口返回了不同日期")
    if r["risk_regime"] == 1:
        print(r["date"], "风险状态1,只记录,不生成卖方信号")
    else:
        print(b["date"], b["lower_strike"], b["upper_strike"])

这段代码只做日期对齐。数据没发布就停,两个接口日期不一致就报错,高风险日只记录、不输出信号。0也不值得庆祝,它只是少了一个劝阻理由。

四、三个稀释案例,不能当成绩单

S-1是公司递交给SEC的注册文件,可能登记新发股份,也可能只是老股东转售。代码先要求dilutive等于1、resale等于0。接着筛市值小于5000万美元的公司,目的就是把新增股份和老股转让分开。

可附图只有ELOX、AKTX、BAFN三行。三行分别是-98.4%、-47.8%、-22.1%,后面标着6天、5天、13天。数字很有冲击力,但持有窗口并不一样,借券费没给,可借数量和成交约束也没给。我不敢把这3个案例当成一张成绩单。

S-1稀释三个案例
仅3个案例,不是完整回测

这段筛选还有两个字段没用上。官方提供filing_timestamp,专门区分盘中与盘后,这段筛选却只看日期。它还提供shares_offered。市值门槛只能说明公司小,不能说明这次新发的规模有多大。

dilutive也只是文件标签,不等于稀释已经完成。注册可能过很久才定价,实际发行规模可能缩小,也可能撤回。要认真统计这类事件的影响,就要把生效与撤回分开,仍待处理的单算,再统一持有窗口。还要用同期非稀释的S-1做对照组,免得把小盘股本身的波动都算到稀释头上。

第三段代码先把研究清单需要的字段补齐,包括披露时间、发行股数以及市值。

filings = get_rows("dilution", date=today)
candidates = [
    row for row in filings
    if row["dilutive"] == 1
    and row["resale"] == 0
    and row["market_cap_at_filing"] is not None
    and row["market_cap_at_filing"] < 50_000_000
]

for row in candidates:
    print(
        row["date"], row["filing_timestamp"], row["ticker"],
        row["shares_offered"], row["market_cap_at_filing"],
    )

再往下做,还要确认披露时刻早于可交易时刻,把离场窗口写进同一条规则。借券能不能拿到、费用多少,也要进损益表。代码筛出候选,只代表研究可以开始。

五、附加信号只告诉你可能波动

Next-Day Movers是一个附加接口。它挑的是下一交易日可能波动更大的股票,只给波动幅度,不给方向。最新记录的return和absolute_move为空。空值不是缺失,只是结果还没发生。如果下载历史时先把空值删掉,等于只留下已经知道结局的样本。历史会被悄悄改写。

它和SPX区间带可以配合研究:一个判断指数会不会留在窄区间,一个寻找个股可能的大波动。但两个信号拼在一起后是否更好,要重新算。期权价格和价差要另取,滑点、停牌也不在接口字段里,官网也没有替这种组合背书。

三段示例代码实际请求4个接口,加上只在正文说明的Movers,全文一共涉及5个。辅助数据同样有发布时间,不能因为它是辅助就跳过时点核对。

六、哪些能核对,哪些不能

还有一项现实变化需要说明。2026年10月26日我查官网时,新REST API和MCP已经要求Pro或Enterprise。公开的数据看板还能看。真想照着跑,先确认账户权限,不要把旧教程里的免费额度提示当成长期承诺。

这篇没有给自己编一套收益。我核对的是四件事。12个月收益重新连乘,521和72重新算命中率。三段Python做语法和模拟返回测试,发布时间同官方文档逐项对照。没有真实API返回,我不敢写接口实测通过。

要把这些方法放进自己的研究,我会先留四类记录,每一类都对应一个容易漏掉的环节。字段可以不复杂,关键是事前记,不要等结果出来再补。

  • 每次请求保存原始JSON和抓取时间,旧文件不覆盖。
  • 每个信号保存发布日期和具体时刻,执行时只取当时可见版本。
  • 每笔研究记录权利金、费用、换手以及资金占用,口径先写死。
  • 每次无法成交都记下原因,借不到与价差过大要分开,数据未发布也单列。

这三套方法最值得研究的地方,恰恰是这些不太好看的执行细节。先把记录补齐,再谈优势。

你手上那张87.86%的成绩单,分母是区间命中,还是扣费后的交易笔数?

参考资料

  • Quant Galore Momentum Index,https://www.alphanume.com/docs/quant-galore-momentum-index
  • S&P 500 0-DTE Strike Band,https://www.alphanume.com/docs/sp-500-0-dte-strike-band
  • S&P 500 Risk Regime,https://www.alphanume.com/sp500-risk-regime
  • Stock Dilution,https://www.alphanume.com/dilution
  • Next-Day Movers与Risk Regime组合说明,https://www.alphanume.com/blog/combine-market-regime-stock-movement-signals
  • API访问与定价,https://www.alphanume.com/pricing

#量化策略 #时点数据 #横截面动量 #0DTE #铁鹰组合 #风险状态 #S1稀释 #NextDayMovers #Python #量化回测

觉得有用的话,把这篇转给同样在做策略研究的朋友。

Published inAI&Invest专栏

Be First to Comment

发表回复