我用长桥 API 给 QQQ 0DTE 策略做回测,差点被数据骗了

做量化交易的人都听过一句话:策略好不好,回测说了算。

但没人告诉你的是——回测本身就会坑你。数据拿错了、信号过滤太严了、参数看起来漂亮但实盘一塌糊涂……这些都是真实发生在我身上的事。

这篇文章记录我用长桥 API 对 QQQ 0DTE 衰竭反转策略做回测时,踩过的每一个坑。如果你也在用长桥做美股策略回测,希望这些经验能帮你少走弯路。


坑 1:yfinance 不靠谱,长桥 API 才是正道

一开始我用 yfinance 下载历史数据,想着免费就行。结果:

  • 频繁被限流(429 Too Many Requests)
  • 1 分钟数据只能拿最近 30 天
  • 数据质量参差不齐,偶有缺失

换了长桥 API 之后,通过 history_candlesticks_by_date() 可以按天拉取 1 分钟 K 线,每天约 241 根(Basic 级别,仅正式盘),Premium 级别含盘前盘后约 960 根。

from longport.openapi import Config, QuoteContext, Period, AdjustType, TradeSessions from datetime import date, timedelta ctx = QuoteContext(Config.from_apikey_env()) # 按天下载,精确控制范围 candles = ctx.history_candlesticks_by_date(    symbol="QQQ.US",    period=Period.Min_1,    adjust_type=AdjustType.ForwardAdjust,    start=date(2026, 4, 14),    end=date(2026, 4, 15),    # 注意:end 不包含这一天    trade_sessions=TradeSessions.All ) print(f"获取到 {len(candles)} 根 K 线")

⚡ 踩坑要点

1. startend 必须是 date 对象,不能是字符串

# ❌ 报错:'str' object cannot be cast as 'date' candles = ctx.history_candlesticks_by_date(..., start='2026-04-14', end='2026-04-15') # ✅ 正确 from datetime import date candles = ctx.history_candlesticks_by_date(..., start=date(2026,4,14), end=date(2026,4,15))

2. 单次最多返回 1000 根 K 线

一天的 1 分钟 K 线(含盘前盘后)刚好接近 1000 根的限制。所以按天循环下载是正确姿势,别想一口气拉一个月的数据:

import time from datetime import date, timedelta all_candles = [] current = date(2025, 7, 1) end_date = date(2026, 4, 18) while current <= end_date:    try:        candles = ctx.history_candlesticks_by_date(            symbol="QQQ.US",            period=Period.Min_1,            adjust_type=AdjustType.ForwardAdjust,            start=current,            end=current + timedelta(days=1),            trade_sessions=TradeSessions.All        )        all_candles.extend(candles)        print(f"  {current}: {len(candles)}根")    except Exception as e:        print(f"  {current}: {e}")    current += timedelta(days=1)    time.sleep(0.2)  # 别太快,防限流

3. timestamp 可能是 datetime 对象

长桥返回的 Candlestick.timestamp 在不同 SDK 版本下可能是 datetime 或 Unix timestamp。直接用 fromtimestamp() 可能炸:

# ✅ 防御性写法 ts = candle.timestamp if isinstance(ts, (int, float)):    ts = datetime.fromtimestamp(ts) # 如果已经是 datetime,直接用


坑 2:5 分钟数据在开盘 1 小时窗口内直接哑火——0 笔交易

第一轮回测,我用 5 分钟 K 线跑了 60 天的数据结果 0 笔交易。

但我当时没当回事,觉得是数据量不够。直到后来用完整的 v6 全过滤策略(双向突破 +ITM 期权 +Black-Scholes 定价)在 5 分钟和 1 分钟数据上做了一次正式对比,结果让我彻底服了:

 5 分钟 K 线1 分钟 K 线
K 线总数40,583 根202,866 根
交易日数536 天536 天
策略窗口09:35-10:50(开盘 1 小时)09:35-10:50(开盘 1 小时)
总交易笔数0 笔451 笔
胜率78.5%
总得分+2139.92%
每年0 笔198 笔
最大回撤25.19%

5 分钟数据在开盘 1 小时内,一笔交易都没触发。

为什么?因为我的策略窗口只有开盘 1 小时(09:35-10:50),5 分钟 K 线在这个窗口里只有约 15 根。再加上全过滤(SMA20 趋势 + 量能 + 动量 +K 线实体),15 根 5 分钟 K 线根本不够过滤条件判断的——指标还没算出来,窗口就关了。

而 1 分钟 K 线在同一窗口内有约 75 根,信号充足,经过 6 层过滤后仍保留 451 笔。

教训:策略的时间尺度和数据的颗粒度必须匹配。 开盘 1 小时的快速行情,5 分钟颗粒度完全跟不上。这不是参数问题,是数据粒度的物理限制。


坑 3:24746 次突破信号只剩 454 笔——6 层过滤漏斗每一层都在"杀人"

切换到 1 分钟数据后,信心满满跑回测。这次不是 0 笔了,但我想搞清楚:过滤条件到底砍掉了多少信号?

写了个诊断脚本,逐层统计每一层过滤通过的次数:

突破信号触发      → 24746 次  ✅ 信号源充足 ↓ 时间窗口过滤(只做 09:35-10:50) 时间窗口通过      →  3535 次  (14.3%)  ⚠️ 85% 被砍 ↓ 跳空过滤(gap < 0.20%) 跳空过滤通过      →  3464 次  (98.0%)  ✅ 跳空不是问题 ↓ SMA20 趋势过滤(做多价格>SMA20,做空<SMA20) SMA20 通过        →  3450 次  (99.6%)  ✅ 趋势几乎不影响 ↓ 量能过滤(成交量 ≥ 20 均量 × 1.2) 量能通过          →  1205 次  (34.9%)  ⚠️ 65% 被砍! ↓ 动量确认(最近 2 根 K 线同向) 动量通过          →   616 次  (51.1%)  ⚠️ 又砍一半 ↓ K 线实体确认(实体 ≥ 0.03%) K 线实体通过       →   454 次  (73.7%) ↓ 最终入场 最终信号          →   454 次  (100%)  ✅ 全部入场

漏斗分析揭示了三个关键真相:

1. 时间窗口是第一大瓶颈(保留 14.3%)。 开盘 1 小时虽然信号质量高,但直接砍掉了 85% 的突破信号。这是有意为之——全天的突破信号太多噪音,开盘时段的信号最有效。

2. 量能过滤是第二大瓶颈(保留 34.9%)。 要求成交量达到 20 日均量的 1.2 倍,直接砍掉了 65% 的信号。这意味着大部分突破发生在缩量状态下,放量突破才是真突破。

3. SMA20 趋势过滤几乎没用(保留 99.6%)。 原以为"做多必须价格在 SMA20 之上"会砍掉很多假信号,实际上 99.6% 的突破信号本身就已经满足这个条件。趋势是结果不是原因——突破本身就隐含了趋势。

诊断代码

如果你也遇到类似问题,可以用这个方法定位瓶颈:

# 逐层统计过滤漏斗 —— 直接告诉你哪里卡住了 stages = {    '突破信号': 0, '时间窗口': 0, '跳空过滤': 0,    'SMA20': 0, '量能': 0, '动量': 0, 'K 线实体': 0, '最终入场': 0, } for i in range(n):    # 第一层:突破信号    if not (prev_close > upper or prev_close < lower):        continue    stages['突破信号'] += 1    # 第二层:时间窗口    if not (9*60+35 <= hour_min <= 10*60+50):        continue    stages['时间窗口'] += 1    # 第三层:跳空    if gap > 0.0020:        continue    stages['跳空过滤'] += 1    # 第四层:SMA20    if sig == 'call' and close < sma20:        continue    if sig == 'put' and close > sma20:        continue    stages['SMA20'] += 1    # 第五层:量能    if volume < sma_vol * 1.2:        continue    stages['量能'] += 1    # 第六层:动量(2 根同向)    if not (连续 2 根同向 K 线):        continue    stages['动量'] += 1    # 第七层:K 线实体    if prev_body < 0.0003:        continue    stages['K 线实体'] += 1    stages['最终入场'] += 1 for stage, count in stages.items():    print(f"  {stage:10s} → {count:5d} 次")

这个漏斗图比任何优化算法都管用。 它直接告诉你哪层过滤太松(浪费计算)、哪层太紧(漏掉机会)、哪层纯属摆设。



坑 4:调参数治标不治本

发现问题后,我尝试调整参数:

big_multfail_thresh交易笔数结果
2.520原始参数,全灭
2.020放宽了,还是没用
1.521终于有 1 笔了
1.511降低衰竭阈值,还是一笔
1.221极端放宽,依然只有 1 笔

结论:参数调整在当前市场环境下效果有限。

这不是参数的问题,是市场状态的问题。最近 QQQ 处于低波动的趋势行情,衰竭反转信号本身就少。策略需要的是高波动、频繁反转的市场环境才能发挥。

这给我的启发是:回测不能只看数字好看不好看,还要看回测数据覆盖了什么样的市场状态。

  • 只回测牛市?策略可能只会做多
  • 只回测低波动?策略可能一单都不触发
  • 必须覆盖牛、熊、震荡至少三种行情

坑 5:时区差点让我多做了一笔假交易

长桥返回的 K 线时间戳是 HKT(UTC+8),不是 UTC,也不是美股东部时间(ET)。

我一开始没注意,直接拿 HKT 时间去判断"美东 9:30 开盘",结果时间全部偏移了 13 个小时(夏令时 12 小时)。这意味着:

  • 美东 9:30 开盘 = 北京时间 21:30
  • 如果代码里写 if hour == 9,实际对应的是北京时间 9 点——根本不在交易时段内

正确的处理方式:

from datetime import datetime import pytz # 长桥返回的是 UTC 时间 utc_time = candle.timestamp  # datetime with tzinfo=UTC # 转换为美东时间 et = pytz.timezone('America/New_York') et_time = utc_time.astimezone(et) # 判断是否在交易时段 if et_time.hour == 9 and et_time.minute >= 30:    # 正式开盘    pass

WSL 环境的额外坑pip3 可能指向系统 Python,而你在虚拟环境里。安装 pytz 要用:

# ❌ pip3 install pytz → 可能装到系统 python 去了 # ✅ /usr/bin/python3 -m pip install pytz --break-system-packages


坑 6:旧数据和新数据合并时格式不统一

我的回测数据来自两个时期:

  • 旧数据:CSV 格式,时间列无时区信息
  • 新数据:从长桥 API 获取,带 UTC 时区

直接 pd.concat() 会报错或者时间对不上。正确做法:

import pandas as pd # 强制统一为 UTC old['Datetime'] = pd.to_datetime(old['Datetime'], utc=True) new['Datetime'] = pd.to_datetime(new['Datetime'], utc=True) # 可选:统一转为美东时间(去掉时区信息,方便按小时筛选)old['Datetime'] = old['Datetime'].dt.tz_convert('America/New_York').dt.tz_localize(None) new['Datetime'] = new['Datetime'].dt.tz_convert('America/New_York').dt.tz_localize(None) # 合并去重 all_data = pd.concat([old, new]).drop_duplicates(subset='Datetime').sort_values('Datetime')


总结:回测中我学到的 6 件事

数据源要可靠。yfinance 免费但不稳定,长桥 API 按天下载 1 分钟 K 线是更好的选择,注意 start/end 必须是 date 对象,单次最多 1000 根。

数据粒度要匹配策略。0DTE 做分钟级交易,必须用 1 分钟数据,5 分钟会漏掉大部分信号。

回测时一定要做信号漏斗分析。逐层统计每个过滤条件通过的次数,快速定位瓶颈在哪。

参数调优有上限。如果市场状态不支持策略逻辑,调什么参数都没用。要看回测数据是否覆盖了不同市场环境。

时区处理是重灾区。长桥返回 UTC 时间,做美股策略需要转成 ET。WSL 下 pip 版本可能串,注意用对 Python。

数据合并前先统一格式。旧 CSV 无时区 + API 数据有时区,直接拼会出 bug。先统一为同一时区再去重合并。


以上是我在 QQQ 0DTE 策略回测过程中的真实踩坑经历。如果你也在用长桥做量化交易,欢迎交流。

本文仅供技术交流,不构成投资建议。

@LongbridgeAI  $Invesco QQQ Trust(QQQ.US)

The copyright of this article belongs to the original author/organization.

The views expressed herein are solely those of the author and do not reflect the stance of the platform. The content is intended for investment reference purposes only and shall not be considered as investment advice. Please contact us if you have any questions or suggestions regarding the content services provided by the platform.