
我用长桥 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. start 和 end 必须是 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_mult | fail_thresh | 交易笔数 | 结果 |
|---|---|---|---|
| 2.5 | 2 | 0 | 原始参数,全灭 |
| 2.0 | 2 | 0 | 放宽了,还是没用 |
| 1.5 | 2 | 1 | 终于有 1 笔了 |
| 1.5 | 1 | 1 | 降低衰竭阈值,还是一笔 |
| 1.2 | 2 | 1 | 极端放宽,依然只有 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 $纳指 100 ETF - Invesco(QQQ.US)
本文版权归属原作者/机构所有。
当前内容仅代表作者观点,与本平台立场无关。内容仅供投资者参考,亦不构成任何投资建议。如对本平台提供的内容服务有任何疑问或建议,请联系我们。



