Insights·2026-07-31

如何用回测验证YouTube上的投资论断 — 从字幕到免费行情API

当一段YouTube视频说"现在就是底部"时,除了相信或不相信之外还有第三条路:把论断转换成数字规则,放到历史数据里跑一遍。这就是回测。需要的工具只有两个 — 把视频字幕导出为文本的yt-dlp,以及无需注册和付费就提供每日收盘价的公开API(美股用纳斯达克,韩股用Naver金融)。步骤有三。先取得字幕,让论断以文字形式存在。再把"暴跌后反弹所以是底部"这样的人话,翻译成条件和数字:"从60日高点下跌超过35%的状态下,当日收盘比前日上涨20%以上,就在当日收盘买入"。然后把规则跑过十年数据。读结果时有两件事是必须的:对照组(如果只是一直持有同样的股票会怎样)和中位数而非平均值。没有对照组,在上涨的市场里任何规则看起来都不错;只看平均值,就会把少数几次暴利误认为是规则的实力。而如果某个论断根本无法写成数字条件,那不是验证失败,而是它本来就无法验证 — 做出这个判定本身就是结果。

回测是什么,为什么需要它

看投资类视频时,总是在同一个地方卡住:有人说"现在就是底部"的那一刻。理由听起来合理,屏幕上的图表也很有说服力,但没有办法核实,于是最终只能在相信和不相信之间二选一。

回测开辟了第三条路。把一个交易判断转换成规则,让这条规则原样跑过历史数据,然后数一数它实际上是否赚钱。它不是预测未来的工具,而是核实你此刻听到的论断在过去是否管用的工具。

人们容易以为这需要专业软件和付费数据,其实不用。一个免费的字幕提取工具,加上两个公开提供股价的API,半天就能跑完。下面是实际验证一段视频时所用的完整顺序。

第一步 — 用yt-dlp把视频字幕导出为文本

首先把内容变成文字。与其把17分钟的视频反复倒带观看,不如把字幕下载成文本文件,这样就能用搜索找到每个论断出现在哪里。

yt-dlp是做这件事的免费命令行工具。名字表明它是youtube-dl的后继版本,能从包括YouTube在内的大量网站下载视频和字幕。Mac上在终端运行brew install yt-dlp,有Python环境则用pip install yt-dlp。

安装完成后,跳过视频只取字幕。--skip-download表示不下载视频文件,--write-auto-sub表示在没有人工字幕时也要取YouTube自动生成的字幕。得到的是vtt格式的字幕文件,里面混有时间戳和重复的行,因此需要做一次清理只留下文本。

清理完成后,17分钟的视频会变成一万字左右的文章。从这一刻起就不必再打开视频了。

终端 — 只下载字幕
# 安装(二选一)
brew install yt-dlp        # macOS
pip install yt-dlp         # Python 环境

# 跳过视频,只取韩语和英语字幕
yt-dlp --skip-download \
       --write-auto-sub --write-sub \
       --sub-lang "ko,en" --sub-format vtt \
       -o "vid.%(ext)s" "<视频地址>"

# 结果: vid.ko.vtt
clean.py — 从vtt中只保留文本
import re

lines, prev = [], None
for line in open("vid.ko.vtt", encoding="utf-8"):
    # 丢弃时间戳行和头部
    if "-->" in line or line.startswith(("WEBVTT", "Kind:", "Language:")):
        continue
    text = re.sub(r"<[^>]+>", "", line).strip()   # 去掉标签
    if text and text != prev:                      # 去掉连续重复
        lines.append(text)
        prev = text

open("transcript.txt", "w", encoding="utf-8").write("\n".join(lines))
print(len(lines), "行")

第二步 — 把论断翻译成数字规则

这一步就是验证的全部。在字幕里找到核心论断,把人话写成的句子转换成计算机能够判定的条件。

比如"跌了很多之后大幅反弹,所以底部已经确认"这句话,原样是无法验证的。跌多少算跌得多?几天内涨多少算大幅反弹?到底什么时候买?把它改写成"从最近60个交易日的最高收盘价下跌35%以上的状态下,当日收盘价比前一日上涨20%以上,就在当日收盘买入",计算机才能在历史数据中找出所有符合条件的日期。

翻译时有一条必须遵守的原则:条件里只能放买入时刻实际能知道的信息。如果混进"触底之后"这类只有事后才知道的表述,就变成了偷看未来的规则,结果必然好看。这叫做前视偏差,也是初学者做的回测在实盘中不管用的最常见原因。

而且在这一步会筛掉一半。因为有些论断根本无法写成数字条件。"是有人故意把它打下去的"这样的句子,连判定真伪的条件都构造不出来。这不是验证失败,而是发现了它本来就无法验证 — 这个判定本身就是结果。仅仅把可验证的论断和不可验证的论断分开,一段视频的可信度就已经相当清楚了。

第三步 — 用免费公开API获取历史行情

意外的是这一步最简单。不需要购买数据。美股由纳斯达克、韩股由Naver金融通过公开地址直接提供每日行情。无需注册,无需API密钥,无需付费。

纳斯达克在api.nasdaq.com下的historical路径接受股票代码和时间区间,返回包含日期、开盘价、收盘价、成交量的JSON。只是响应中的价格是带美元符号和逗号的字符串,需要一行处理转成数字,而且可查询区间大致到最近十年。

韩股则在Naver金融的行情地址中填入六位股票代码和起止日期即可。三星电子是005930,SK海力士是000660。响应不是标准JSON而是混有单引号的数组形式,无法直接解析,因此要替换引号或用正则表达式提取数值。

两者都是按照人用浏览器访问的前提做的,所以从程序调用时,附上看起来像普通浏览器的请求头(User-Agent)比较安全。短时间内调用过多也会被暂时拦截,因此每个股票间隔约0.5秒获取,并把取到的数据保存成文件重复使用。

fetch_us.py — 纳斯达克每日收盘价
import json, urllib.request

UA = {"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
                    "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126 Safari/537.36"}

def daily(symbol, asset="stocks", frm="2016-01-01", to="2026-07-31"):
    url = (f"https://api.nasdaq.com/api/quote/{symbol}/historical"
           f"?assetclass={asset}&fromdate={frm}&todate={to}&limit=99999")
    req = urllib.request.Request(url, headers=UA)
    rows = json.load(urllib.request.urlopen(req, timeout=30))
    rows = rows["data"]["tradesTable"]["rows"]

    out = []
    for r in rows:
        close = float(r["close"].replace("$", "").replace(",", ""))   # "$207.12" → 207.12
        mm, dd, yy = r["date"].split("/")                              # "07/30/2026"
        out.append({"d": f"{yy}-{mm}-{dd}", "c": close})
    out.sort(key=lambda x: x["d"])
    return out

bars = daily("MU")
print(len(bars), bars[-1])
fetch_kr.py — Naver金融每日收盘价
import re, urllib.request

UA = {"User-Agent": "Mozilla/5.0"}

def daily_kr(code, start="20160101", end="20260731"):
    url = ("https://api.finance.naver.com/siseJson.naver"
           f"?symbol={code}&requestType=1&startTime={start}&endTime={end}&timeframe=day")
    req = urllib.request.Request(url, headers=UA)
    text = urllib.request.urlopen(req, timeout=30).read().decode("utf-8")

    # 不是标准JSON而是单引号数组,用正则提取
    # ['20260730', 开盘, 最高, 最低, 收盘, 成交量, 外资持股率]
    rows = re.findall(r"\['(\d{8})',\s*[\d.]+,\s*[\d.]+,\s*[\d.]+,\s*([\d.]+)", text)
    out = [{"d": f"{d[:4]}-{d[4:6]}-{d[6:]}", "c": float(c)} for d, c in rows]
    out.sort(key=lambda x: x["d"])
    return out

print(daily_kr("005930")[-1])   # 三星电子

第四步 — 跑规则,但一定要和对照组一起看

数据齐了之后,找出所有符合第二步条件的日期,计算此后一周、一个月、三个月的收益率。到这里都是机械的工作。

真正重要的是接下来。一定要同时计算对照组。如果不管信号只是一直持有同样的股票,同期能赚多少;指数(比如标普500)涨了多少 — 把它们并排放好。漏掉这一步,在上涨的市场里任何规则看起来都不错,因为那是随便哪天买都赚钱的区间。

实际用AI相关股票篮子跑一遍会发现,暴跌后买入信号的三个月收益率平均12.6%,看起来不错。但同样的股票随便哪天买、持有三个月的平均值也是12.6%。也就是说信号创造的部分接近零。如果没算对照组,这条规则看起来会像了不起的发现。

样本数也要一并写上。条件卡得越紧信号越少,而只有三四次的规则,其平均收益率是偶然而不是统计。把条件换成多种组合分别跑(参数扫描),如果看起来好的组合全都只有三四个样本,那不是发现而是过拟合。

backtest.py — 信号表现与对照组同时计算
import statistics as st

def backtest(bars, dd_thresh=-0.35, pop=0.20, horizon=63):
    """从60日高点跌破dd_thresh后,单日上涨pop以上 → 当日收盘买入"""
    signals, baseline = [], []

    for i in range(60, len(bars) - horizon):
        c, prev = bars[i]["c"], bars[i - 1]["c"]
        high60 = max(b["c"] for b in bars[i - 60:i + 1])
        fwd = bars[i + horizon]["c"] / c - 1        # horizon日后的收益率

        baseline.append(fwd * 100)                   # 对照组:所有交易日
        if c / prev - 1 >= pop and c / high60 - 1 <= dd_thresh:
            signals.append(fwd * 100)                # 只取信号出现日

    return signals, baseline

sig, base = backtest(bars)
print(f"信号   n={len(sig):4d}  平均 {st.mean(sig):6.1f}%  中位数 {st.median(sig):6.1f}%")
print(f"对照组 n={len(base):4d}  平均 {st.mean(base):6.1f}%  中位数 {st.median(base):6.1f}%")
# 信号的平均值若超不过对照组,这条规则就没有实力

如何读结果 — 只看平均值会读反

数字出来后人们首先看平均值,而在回测里只看平均值几乎必然导致误读。必须把平均值和中位数并排放。

有一个很清楚的实例。把"从60日高点下跌超过35%的状态下单日反弹20%以上"这个条件跑过十年数据,会抓到24个信号。这24个的三个月后收益率平均是38.2%,看起来很惊人。但中位数是-1.0%,盈利的只有24个中的11个,不到一半。

发生了什么?是几次暴利(其中一次三个月258%)独自把平均值拉了上去。剩下的一半在原地继续下跌。只看平均值就会得出"暴跌后反弹是强力买入信号"的结论;同时看中位数,得到的是完全相反的结论 — 这是一场胜率46%、期望值全押在少数暴利上的赌博。

另一个要当心的是幸存者偏差。用今天表现好的股票组成篮子去跑过去十年,结果当然好看,因为这十年里消失的股票不在名单上。如果篮子里混进了上市才一年的股票,就等于在用那之前根本不存在的股票计算收益率。要处理成每个时点只纳入当时确实存在的股票,如果有因数据不足而剔除的股票,就在结果里一并写明。

常见的卡点

行情API突然返回429或Too Many Requests,说明短时间内调用过多。每个股票间隔0.5秒获取,并且务必把取到的数据保存成文件,重跑时不要再次调用。

也有完全取不到字幕的视频。既没有人工字幕,自动字幕也被关闭,这时需要单独的语音识别环节。另外自动字幕经常把数字和专有名词弄错,所以从字幕中提取的数值不要直接当作事实使用,要回到原始出处再确认一次。

美股代码在公司合并或改名后会消失。验证过去的案例时如果数据返回0条,先确认是不是股票已经不存在了。悄悄剔除这类股票会让结果比实际更好看。

最后要注意不要把时间不够的信号计入平均。如果看的是三个月表现,而信号发生在两周前,那一条还没有结果。硬塞进去会让近期行情污染整个结果集。没有结果的样本要从统计中剔除,并写明剔除了几条。