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

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

核心结论:冬令时切换后,美东常规交易仍是09:30—16:00,但对应北京时间晚一小时;2026年11月2日为北京22:30—次日05:00。处理顺序是:先查交易日历,再确认当天交易安排,最后换算北京时间。北京时间周二凌晨收盘,仍属于美国周一交易日。半日交易和夜盘不能直接套用普通交易日表。

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

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

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

核心结论:美东仍是09:30,冬令时后北京时间晚一小时

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

美国交易日期美东常规交易时段对应北京时间
10月30日09:30—16:0010月30日21:30—10月31日04:00
11月2日09:30—16:0011月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”并排保留。读者知道自己在比较哪天的收盘,技术同事也有明确的日期依据。直接把北京时间的日期截出来填进“美国交易日”,就会把这条记录放错一天。

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

盘前、常规、盘后北京时间对照:以11月2日完整交易日为例

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

时段美东时间对应北京时间
盘前11月2日04:00—09:3011月2日17:00—22:30
常规交易11月2日09:30—16:0011月2日22:30—11月3日05:00
盘后11月2日16:00—20:0011月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 = "https://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也提醒,延长时段的流动性、价格波动和交易场所条件与常规时段存在差异,不能只换一个标签就按相同条件理解价格。

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

FAQ:美股行情API冬令时换算常见问题

Q1:冬令时切换后,美股常规交易在北京时间几点开盘?
以2026年11月2日为例,美东09:30对应北京22:30。冬令时期间纽约与北京时差13小时,不能全年固定加12小时或13小时。

Q2:为什么北京11月3日05:00收盘,仍记为美国11月2日?
该时刻对应纽约11月2日16:00,记录的是美国11月2日常规收盘。复盘表应并排保留美国交易日与北京时间,不能直接截取北京日期。

Q3:TickDB美股交易时段返回包含哪些窗口?
本次返回包含美东04:00—09:30、09:30—16:00、16:00—20:00三个窗口。09:30—16:00记录未返回trade_session字段,示例按Nasdaq常规时段核对后标注“常规”,并保存原始响应。

Q4:半日交易日应该怎么处理?
先查交易日历识别半日,再核对当天安排。通用时段查询没有交易日期参数,半日标记也不直接给出13:00;应停下通用时刻表,核对后再生成。2026年11月27日被列为半日,Nasdaq公布美东13:00提前收市;11月26日感恩节休市。

Q5:盘前起点为什么有04:00和07:00两种说法?
Nasdaq列出盘前从04:00开始;FINRA投资者资料用07:00作为典型盘前起点,并说明券商可能设置不同时间、产品和订单限制。产品应写明采用哪套时段,不能把单一起点当统一规则。

Q6:夜盘能套用本文的盘前、常规、盘后表吗?
不能。夜盘另有交易安排与日期归属规则,本文这张表只覆盖盘前、常规、盘后三个窗口。

Q7:行情页最容易漏掉哪四处时间检查?
开盘提醒是否按目标交易日换算、行情标签是否匹配时段、图表横轴是否显示时区和跨日日期、复盘日期是否分列美国交易日与北京时间。

参考资料

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


0
0
0
0
评论
未登录
暂无评论