美股行情API遇上冬令时:盘前盘后时间怎么对齐?

用户头像sh_*599ojc
2026-11-03 发布

摘要:负责美股行情页和开盘提醒的产品经理,冬令时切换后需要一起核对交易时段、北京时间和交易日期。本文结合TickDB交易时段、交易日历的真实返回,解释盘前盘后怎样换算,为什么凌晨收盘仍属于美国前一个交易日,并提供时间对照表、可运行换算示例与页面检查清单,处理半日交易等容易遗漏的例外。

2026年11月2日,美股进入本次冬令时切换后的首个交易日,常规交易在北京时间22:30开盘。

假设一张行情页仍按旧配置,在21:30发出开盘提醒,并把标签切成“盘中”。此时常规交易还没开始,页面若展示盘前报价,用户就容易按盘中行情理解。产品经理排查时,需要先核对标签和提醒采用的时间规则。

调整这类设置,可以先确定美国交易日及当天安排,再换算成北京时间。本文用11月3日实际取得的TickDB美股交易时段和日历返回,生成切换前后的对照表,沿着提醒、行情标签和复盘日期逐项检查。

一分钟看懂:美东还是09:30,北京时间为什么晚了

image.png

图1:AI生成解释图。按2026年指定日期的常规交易安排换算,非实时行情截图。

先把切换前后的两个交易日放在一起:

美国交易日期 美东常规交易时段 对应北京时间
10月30日 09:30—16:00 10月30日21:30—10月31日04:00
11月2日 09:30—16:00 11月2日22:30—11月3日05:00

NIST公布的2026年规则显示,美国夏令时在11月1日结束。切换后,纽约与北京的时差从12小时变成13小时,所以美东同一个09:30,对应的北京时间晚了一小时。

美东时间常写作ET,它会随日期采用夏令时或标准时间。这里按纽约时区计算,不能全年固定加12小时,也不能全年固定加13小时。

产品里若只保存“每天21:30提醒”,下一次切换时仍需人工修改。把提醒写成“美国交易日的纽约时间09:30”,再换算为北京时间,日期与时区就一起参与了计算。

北京已经是周二,美股周一的常规交易才刚收盘

北京时间11月3日05:00,对应纽约11月2日16:00。这一刻记录的是美国11月2日的常规收盘。

因此,复盘表可以把“美国交易日:11月2日”与“北京时间:11月3日05:00”并排保留。读者知道自己在比较哪天的收盘,技术同事也有明确的日期依据。直接把北京时间的日期截出来填进“美国交易日”,就会把这条记录放错一天。

行情页可以采用同样的写法:标题保留美国交易日期,时间轴按用户所在时区显示。跨过北京时间零点时,日期标签仍有依据可查。

盘前、常规、盘后,分别落在北京时间哪一段

本次TickDB美股交易时段返回中,包含美东04:00—09:30、09:30—16:00、16:00—20:00三个窗口。结合Nasdaq常规及延长时段安排核对后,以11月2日这个完整交易日为例,可以生成下面的表:

时段 美东时间 对应北京时间
盘前 11月2日04:00—09:30 11月2日17:00—22:30
常规交易 11月2日09:30—16:00 11月2日22:30—11月3日05:00
盘后 11月2日16:00—20:00 11月3日05:00—09:00

这张表能帮助产品同事检查标签,也能解释一个常见疑问:北京时间早晨看到的盘后记录,可能仍对应美国前一天的交易安排。

使用时还应核对自己所用券商和数据服务的范围。Nasdaq列出的盘前从04:00开始;FINRA的投资者资料用07:00作为典型盘前起点,并说明券商可能设置不同的可交易时间、产品和订单限制。两份材料描述的范围并不完全相同。

行情产品写明“本页采用哪套时段”,比把一个起点当成所有平台的统一规则更有用。夜盘另有交易安排与日期归属规则,本文这张盘前、常规、盘后表不直接套用到夜盘。

冬令时时间换对了,还要看看当天是否提前收市

本次查询中,TickDB日历将2026年11月27日列在半日交易日列表。Nasdaq公布当天美东13:00提前收市;11月26日感恩节休市。

如果程序仍套用普通交易日16:00收市,即使时区换算正确,页面也会把收市时间标晚。处理顺序应当是:先查交易日历,再确认当天的交易安排,最后换算北京时间。

这几项输入各有分工:

输入 回答的问题 本例中的作用
交易日历 当天是全日、半日,还是未列为交易日 识别11月27日需要另查具体安排
当天交易安排 当地几点开始、几点结束 半日收市时刻采用Nasdaq公布的13:00
日期与时区规则 这些时刻在北京时间如何显示 按指定日期换算,保留跨日信息

本次通用时段查询没有交易日期参数,日历中的半日标记也没有直接给出13:00。程序遇到这类日期,应停下通用时刻表的生成,核对当天安排后再继续。

把方法接进行情页:一次取数,生成一张检查表

本次使用的两个TickDB REST入口是:

GET https://api.tickdb.ai/v1/market/trading-sessions
GET https://api.tickdb.ai/v1/market/trade-days

交易时段请求指定market=US;交易日历另带查询日期范围。参数与使用条件见TickDB交易时段文档和交易日历文档。

11月3日的真实日历返回,包含10月30日、11月2日和11月3日三个全日交易日。另一组查询中,11月26日未列为交易日,11月27日被列为半日交易日。

实现时还遇到一个细节:09:30—16:00这一条记录没有返回trade_session字段。下面的示例把这个精确窗口与Nasdaq常规时段核对后标注为“常规”,同时保存原始响应。遇到其他无法识别的窗口,程序会停止,留给使用者检查。

给技术同事:可直接运行的示例

使用Python 3.10+、系统curl和可用的IANA时区数据。若环境提示缺少时区数据,可先安装tzdata。将密钥安全配置到环境变量TICKDB_API_KEY,把下面代码保存为us_session_table.py,运行:

python3 us_session_table.py us_time_result

输出目录必须尚不存在,以免覆盖已有记录。代码会保存原始响应、请求时间与参数,以及可以用表格软件打开的CSV。

import csv
import json
import os
import re
import subprocess
import sys
from datetime import datetime, timedelta, timezone
from pathlib import Path
from urllib.parse import urlencode
from zoneinfo import ZoneInfo

key = os.environ.get("TICKDB_API_KEY", "")
if not re.fullmatch(r"[A-Za-z0-9_-]{16,256}", key):
    raise SystemExit("请先安全配置 TICKDB_API_KEY")
out = Path(sys.argv[1] if len(sys.argv) > 1 else "us_time_result")
out.mkdir(parents=True, exist_ok=False)
ny, bj = ZoneInfo("America/New_York"), ZoneInfo("Asia/Shanghai")

def fetch(name, path, params):
    url = "//api.tickdb.ai" + path + "?" + urlencode(params)
    config = f'url = "{url}"\nheader = "X-API-Key: {key}"\n'
    r = subprocess.run(
        ["curl", "--config", "-", "--silent", "--show-error",
         "--max-time", "20", "--write-out", "\n%{http_code}"],
        input=config, text=True, capture_output=True, timeout=25)
    body, _, http = r.stdout.rpartition("\n")
    (out / f"{name}.json").write_text(
        body.replace(key, "[REDACTED]"), encoding="utf-8")
    meta = {"path": path, "params": params, "http": http,
            "curl_exit": r.returncode,
            "queried_at_utc": datetime.now(timezone.utc).isoformat()}
    (out / f"{name}.meta.json").write_text(
        json.dumps(meta, ensure_ascii=False), encoding="utf-8")
    if r.returncode or http != "200":
        raise RuntimeError(f"{name} 请求失败,检查保存的响应与网络")
    obj = json.loads(body)
    if obj.get("code") != 0:
        raise RuntimeError(f"{name} 业务状态异常,检查保存的响应")
    print(f"{name}: HTTP={http}, API={obj['code']}")
    return obj["data"]

try:
    markets = fetch("sessions", "/v1/market/trading-sessions", {"market": "US"})
    days = fetch("days", "/v1/market/trade-days",
                 {"market": "US", "beg_day": "20261030", "end_day": "20261103"})
    holiday = fetch("holiday", "/v1/market/trade-days",
                    {"market": "US", "beg_day": "20261126", "end_day": "20261127"})
    sessions = next(x for x in markets if x["market"] == "US")["trading_sessions"]
    full = set(days["trade_days"] + holiday["trade_days"])
    half = set(days["half_trade_days"] + holiday["half_trade_days"])
    rows = []
    for day in ["20261030", "20261102", "20261103", "20261126", "20261127"]:
        if day in half or day not in full:
            status = "半日交易,另查当天时刻" if day in half else "未列为交易日"
            rows.append([day, status, "", "", "", "", ""])
            continue
        for s in sessions:
            kind = s.get("trade_session")
            # 本篇按Nasdaq常规时段核对缺失类型,不改写原始响应。
            if kind is None and (s["begin_time"], s["end_time"]) == (930, 1600):
                kind = 0
            if kind not in (0, 1, 2):
                raise ValueError("遇到未处理时段,请核对规则")
            def local(hhmm):
                return datetime.strptime(day, "%Y%m%d").replace(
                    hour=hhmm // 100, minute=hhmm % 100, tzinfo=ny)
            a, b = local(s["begin_time"]), local(s["end_time"])
            if b <= a:
                b += timedelta(days=1)
            label = {0: "常规", 1: "盘前", 2: "盘后"}[kind]
            rows.append([day, "全日交易", label, a.isoformat(), b.isoformat(),
                         a.astimezone(bj).isoformat(), b.astimezone(bj).isoformat()])
    with (out / "time_table.csv").open("w", encoding="utf-8-sig", newline="") as f:
        writer = csv.writer(f)
        writer.writerow(["美国日期", "状态", "时段", "美东开始", "美东结束",
                         "北京开始", "北京结束"])
        writer.writerows(rows)
    print(f"已保存 {len(rows)} 行:{out / 'time_table.csv'}")
except (OSError, ValueError, KeyError, StopIteration, RuntimeError,
        subprocess.TimeoutExpired) as exc:
    raise SystemExit(f"未完成:{type(exc).__name__};核对原始响应、环境与字段")

本文这份代码已于2026年11月3日实际运行,三次请求均返回HTTP 200、业务码0,输出11行CSV:三个完整交易日各生成盘前、常规、盘后记录,共9行;另有11月26日未列为交易日、11月27日半日交易的两行状态。

代码把半日交易留作待核对状态,不替读者编出具体时刻。接入自己的产品时,可增加带来源和更新日期的特殊交易安排,再把提醒、标签与图表共同接到这份时间表。

产品验收时,先查这四处

位置 检查什么 本例中的核对点
开盘提醒 是否按目标交易日换算,而非固定北京时间 11月2日对应22:30
行情标签 “盘前”“盘中”“盘后”是否使用匹配范围的安排 21:30尚未进入常规时段
图表横轴 是否明确显示时区及跨日日期 北京时间零点后仍可能处于美国当日常规交易
复盘日期 美国交易日与北京时间是否分列 美国11月2日收盘对应北京11月3日05:00

这张表检查的是时间设置。具体报价属于哪个对象、数据时间是否够新,还要一起查看行情返回。FINRA也提醒,延长时段的流动性、价格波动和交易场所条件与常规时段存在差异,不能只换一个标签就按相同条件理解价格。

如果你正在调整美股行情页,可以先选一个完整交易日,把提醒、标签和复盘日期放在同一张表里核对,再补一个半日交易日。普通日期和例外日期都能说清,技术同事才知道该怎样实现,使用页面的人也能看明白每个时间的含义。

参考资料

  1. NIST:《Daylight Saving Time Rules》,2026年夏令时起止安排。
  2. Nasdaq:《Nasdaq Trading Schedule》,2026年休市、提前收市与常规及延长时段安排。
  3. FINRA:《Extended-Hours Trading: Know the Risks》,2024年7月31日,延长时段特点与券商限制说明。
  4. TickDB:交易时段、交易日历官方文档;2026年11月3日REST真实查询及随文代码运行记录。

本文以Nasdaq相关安排和指定日期演示行情时间处理;报价来源及券商可交易范围按实际服务核对。内容用于数据接入与产品设置,不构成投资建议。

评论