Python 股票行情 API 实战:用 React 构建实时行情与 K 线面板,支持美股历史数据下载
TickDB Research ·
Python 股票行情 API 实战:用 React 构建实时行情与 K 线面板,支持美股历史数据下载
接入一个行情 API,通常只需要几行请求代码。把 A 股、港股、美股、外汇、黄金、指数和加密资产放进同一个可用的页面,还需要解决另一组问题:代码可能存在歧义,涨跌幅的统计窗口不同,部分品种没有成交量,股票休市后仍然会返回最后一条报价,历史 K 线的最后一根也可能继续变化。
这篇文章用 React 和 Python 实现一个跨资产行情面板。左侧是可筛选的自选列表,右侧依次展示当前报价、买卖盘口、逐笔交易明细和历史走势,支持周期切换、股票复权方式选择以及 CSV 下载。报价通过 WebSocket 更新,盘口和最近成交按当前品种查询;所有上游请求都经过 Python 服务,API Key 留在服务端。
本文的“全资产”限定为股票(A 股、港股、美股)、外汇、贵金属、指数和加密资产。示例每类选择一个代表品种,共七个;覆盖资产类别不等于加载全部证券,也不包含期货、期权和完整债券市场。本文面板与截图采用 TickDB 实际查询返回的数据,默认以 capture 模式读取保存的原始响应;live 模式用于配置服务端密钥后的持续接入,demo 仅保留为显式选择的合成测试模式。快照是真实查询结果,但不是持续实时推送。
先把面板的交付范围定下来
这一版解决连续的用户任务:找到想看的品种,查看当前报价、委托档位与最近成交,判断数据口径,再查看和下载历史走势。
| 页面区域 | 完成的任务 | 必须保留的信息 |
|---|---|---|
| 自选列表 | 按资产类别筛选,按代码或名称搜索 | 品种类型、价格单位、涨跌幅窗口 |
| 当前报价 | 查看最新价、买卖报价和扩展时段 | 行情时间、行情年龄、时段标签 |
| 买卖盘口 | 查看买盘与卖盘各档价格、数量 | 实际返回档位、盘口时间、查询时间 |
| 逐笔交易明细 | 查看最近成交价、数量、时间、方向和 ID | 原始数量口径、毫秒时间、缺失 ID、结果窗口 |
| 历史走势 | 切换分钟、小时和日线,选择股票复权 | 返回数量、实际首尾时间、复权方式 |
| CSV 导出 | 下载当前查询返回的 K 线 | UTC 时间、查询时间、数据模式、尾根提示 |
| 状态区 | 区分浏览器断线和上游异常 | 连接状态、等待数据、重试或停止原因 |
工程只用一个 Python 进程维护自选列表的上游订阅。浏览器通过自己的 WebSocket 连接读取后端当前状态,每秒接收一帧合并后的报价。这是本例的页面刷新策略,不是上游的推送频率,更不是逐笔数据归档。
盘口和逐笔成交采用独立的 REST 查询:进入品种详情时读取,完成一次查询后等待 3 秒再查,后端缓存成功结果 3 秒。页面展示的是最新一份盘口快照和最近 100 笔成交窗口,不能把这个刷新策略理解为逐事件推送或完整成交采集。
文件分工也保持直接:core.py 保存品种样本和纯数据转换,server.py 负责 REST、上游 WebSocket、缓存与浏览器接口,frontend/src.jsx 负责 React 交互,style.css 负责布局。依赖与启动命令随源码一起提供。
用产品元数据组织 A 股 港股 美股和其他资产
股票代码的交易所后缀只能解决一部分识别问题。黄金和外汇可能属于同一个上游产品类型,但在页面上应该出现在不同分类;指数的单位是点,也不能统一加上美元符号。
因此需要区分三个概念:type 用于请求上游,group 用于页面分类,unit 用于解释数值。例如黄金可以使用 type=forex、group=贵金属;两者服务于不同任务,并不矛盾。本文的样本单位是逐项配置的,不能推广成“根据代码后缀推断所有证券币种”。
INSTRUMENTS = [
{"symbol": "600519.SH", "type": "stock", "group": "A股"},
{"symbol": "700.HK", "type": "stock", "group": "港股"},
{"symbol": "AAPL.US", "type": "stock", "group": "美股"},
{"symbol": "EURUSD", "type": "forex", "group": "外汇"},
{"symbol": "XAUUSD", "type": "forex", "group": "贵金属"},
{"symbol": "SPX", "type": "indices", "group": "指数"},
{"symbol": "BTCUSDT", "type": "crypto", "group": "加密资产"},
]
这是一份教程自选清单,不是完整产品目录。扩展成证券搜索时,应分页读取产品查询接口的 products,保留 symbol、type、market、currency 和 is_active,以实际返回结果建立目录。接口提供 limit 和 offset,应根据 pagination 遍历;不能把默认第一页当成全部产品。品种出现在目录里,也不代表当前账号已经拥有对应实时行情权限。
对外提供查询时,后端只接受清单中的代码、支持的周期和复权枚举,不接受浏览器传入任意上游 URL。这样既能明确教程的范围,也能防止应用变成任意请求代理。
将首次快照和持续更新接到同一个状态表
页面打开后先需要可显示的当前状态,之后才是持续变化。REST 行情快照适合初始化与重连后的状态修复;WebSocket 适合持续接收新事件;历史 K 线用于详情页,独立按需查询。

图 1:示例应用的数据流。后端保存密钥、规范化数据并向浏览器分发;这是接入方案示意,不是服务商内部架构或性能实测。
下面的最小请求可以先验证三地股票行情 API 的请求路径。环境变量应在服务端配置,不写进 React 的源码或 VITE_ 前缀环境变量。
curl --fail-with-body --get \
'https://api.tickdb.ai/v1/market/ticker' \
-H "X-API-Key: ${TICKDB_API_KEY}" \
--data-urlencode 'symbols=600519.SH,700.HK,AAPL.US' \
--data-urlencode 'type=stock'
HTTP 成功后仍要检查 JSON 中的业务 code 是否为 0。普通快照一次最多查询 50 个代码;扩容时既要分批,也要按账号实际限额调度。本例按 stock、forex、indices、crypto 分别请求和订阅,避免把 type=stock 错误应用于混合资产。
Python 的请求入口可以集中为一个函数,统一设置超时、鉴权和错误处理:
async def fetch_data(client, path, params, api_key):
response = await client.get(
"https://api.tickdb.ai" + path,
params=params,
headers={"X-API-Key": api_key},
timeout=15,
)
response.raise_for_status()
payload = response.json()
if payload.get("code") != 0:
raise RuntimeError(f"Upstream code: {payload.get('code')}")
return payload["data"]
这个片段需要一个 httpx.AsyncClient,完整后端通过 FastAPI 的生命周期创建和关闭它。不要每个品种、每个浏览器页面都新建一套上游连接;本例共享一份有明确样本范围的状态表。
统一字段时保留缺失值和统计窗口
跨资产行情面板最容易出现的错误,是把没有返回的值显示成零。外汇有买卖报价,不代表一定有与股票同口径的成交量;指数只返回价格与时间,也不应该被强行填出一套涨跌数据。
价格在传输和保存阶段保留十进制字符串,真正绘图时再转换成 JavaScript 数值。以下转换保留 "0",同时拒绝空值、布尔值、NaN 和无穷大:
from decimal import Decimal, InvalidOperation
def number(value):
if value is None or isinstance(value, bool):
return None
if str(value).strip() == "":
return None
try:
value = Decimal(str(value))
return str(value) if value.is_finite() else None
except InvalidOperation:
return None
volume_24h 这样的字段名也不能代替业务解释。当前文档把加密资产的常见口径解释为滚动 24 小时,传统市场通常为当日或当前交易时段。因此页面给每条涨跌幅保留窗口标签,没有把七个品种放进一个混合涨幅排行榜。外汇买卖价差如果用于显示,可以计算 ask - bid,但不能在没有品种点值元数据时把它直接标成“点数”。
主快照和美股扩展时段需要分开存储。REST 的盘前、盘后、夜盘是嵌套对象,WebSocket 的扩展时段消息通过 trade_session 标识;统一后的键采用 type:symbol:session。这样夜盘价格不会覆盖主快照后,却继续带着主快照的成交量或涨跌幅。
普通消息没有扩展时段标签时,本例使用 base,页面称“主快照”,不据此推断交易所此刻处于常规交易。
做好外汇 WebSocket 的乱序和断线恢复
下面是单个外汇品种的最小订阅示例,需要 websockets 库和服务端密钥。它用于看清连接、订阅与接收消息的关系;完整工程再加入恢复、状态表和浏览器分发。不要记录包含密钥的连接 URL。
import asyncio
import json
import os
from urllib.parse import urlencode
from websockets.asyncio.client import connect
async def main():
query = urlencode({"api_key": os.environ["TICKDB_API_KEY"]})
url = "wss://api.tickdb.ai/v1/realtime?" + query
async with connect(url, ping_interval=20, ping_timeout=20) as ws:
await ws.send(json.dumps({
"cmd": "subscribe",
"data": {
"channel": "ticker", "type": "forex",
"symbols": ["EURUSD"],
},
}))
async for raw in ws:
message = json.loads(raw)
if message.get("cmd") == "ticker":
print(message["data"])
elif message.get("cmd") == "error":
raise RuntimeError("订阅错误,请检查返回代码与权限")
asyncio.run(main())
外汇 WebSocket 接入真正需要解决的是消息如何进入状态表。先保存最新事件时间,再决定新消息能否覆盖旧状态:
def apply_quote(book, row):
if row is None:
return False
old = book.get(row["key"])
if old and row["event_ms"] < old["event_ms"]:
return False
book[row["key"]] = row
return True
这段代码采用整条观测替换。如果新消息没有买价、成交量或涨跌幅,就显示缺失,不悄悄沿用上一条的可选字段。好处是语义清楚;代价是部分字段可能暂时变为 —。正式系统若希望保留这些字段,需要为每个字段保存自己的更新时间和来源。
相同时间戳的消息按到达顺序覆盖。这是本例的明确取舍:没有服务端单调序列号时,毫秒时间戳不足以证明两个事件的严格先后。这里实现的是最新状态展示,不能将其描述为逐笔不丢失或严格有序系统。
图 2:重连后恢复当前状态的步骤。补快照只修复最新状态,不证明断线期间的所有成交事件已经补齐。
完整后端按以下顺序处理连接:建立 WebSocket,按类型发送订阅,再请求 REST 快照,最后读取连接缓冲中的消息。流消息和快照都经过同一条事件时间检查,较早的消息不会让已显示的价格倒退。Python 客户端在取快照期间缓冲消息,缓冲队列设有上限;这不是无限事件存储。
连接恢复后重新订阅。网络断线和可重试服务错误按 1 秒、5 秒、30 秒分档等待;收到鉴权、权限或未知业务错误则停止自动重试,显示错误代码。遇到 429 还应核对限额,正式部署可进一步遵循 Retry-After,避免错误配置造成持续请求。
示例使用 WebSocket 协议级 Ping 检测失活连接。协议心跳正常,只能说明连接仍有响应;某个品种可能休市、没有新成交、没有权限或未正确订阅。页面因此分别展示浏览器到后端的连接状态、后端上游状态以及每条报价的行情时间。
“行情年龄”计算为当前时间减事件时间。它受交易活跃程度、市场时段、上游时间语义和本机时钟影响,不能叫网络延迟。休市状态需要交易日历与交易时段证据,本例没有把“超过 60 秒没更新”直接判断为异常或休市。
把当前报价 盘口和逐笔成交拆成三种状态
当前报价回答“最新价格是多少”;盘口回答“返回的各档买卖报价及数量是多少”;逐笔成交回答“最近发生了哪些成交”。这三者需要不同的数据模型。价格改变不能自动生成一笔成交,盘口数量减少也不能直接推断为成交量。
页面始终保留三个区域,但是否能显示数据取决于品种和权限。当前示例为三地股票和加密资产接入盘口与成交查询。外汇、贵金属和指数不在本次所用接口文档的这两项支持市场清单中,因此显示“未配置此能力”,不制造空白五档;外汇和贵金属已有的 bid/ask 仍放在当前报价区。文档未列出不等于所有供应商都没有这种数据。
先用下面的请求验证一个港股品种的盘口结构:
curl --fail-with-body --get \
'https://api.tickdb.ai/v1/market/depth' \
-H "X-API-Key: ${TICKDB_API_KEY}" \
--data-urlencode 'symbol=700.HK' \
--data-urlencode 'type=stock'
盘口返回 bids 和 asks,每个档位是 [price, quantity],另有品种与毫秒时间戳。转换时校验返回品种、价格、数量和时间,再将买盘按价格降序、卖盘按价格升序排列。价格保留十进制字符串;数量为零与字段缺失分开处理。本文使用的是 REST 全量响应,每次替换当前展示窗口,不推断它是需要累加的增量委托簿。
界面每侧最多展示 10 档,并另列“上游实际返回档位”。只有买一卖一时只画一行,不补出不存在的买二或卖二。一侧为空而另一侧有记录时,缺少的一侧显示 —;两侧都空才显示“本次查询未返回记录”。实际可获得档位由市场、品种和权限决定,在线接入时以响应为准。
逐笔交易明细使用独立接口,不能从 ticker 的累计成交量差分拼出来:
curl --fail-with-body --get \
'https://api.tickdb.ai/v1/market/trades' \
-H "X-API-Key: ${TICKDB_API_KEY}" \
--data-urlencode 'symbol=700.HK' \
--data-urlencode 'type=stock' \
--data-urlencode 'limit=100'
成交表保留 price、quantity、timestamp、side 和可用的 id,按事件时间倒序显示。方向只翻译上游的 buy、sell、neutral,不自行推导主动买入、资金流向或主力行为。数量未确认单位前显示原始值,不擅自标成“股”或把“手”乘以 100。相同毫秒的多笔成交保留返回顺序,不声称已经确定严格先后。
最近成交窗口每次整体替换,避免轮询重叠区被重复追加。在单次响应内,仅对有 ID 且 ID 与时间相同的记录去重;没有 ID 的相同时间、价格、数量记录仍保留,因为它们可能是两笔真实成交。React 行键可以使用当前窗口的位置,它只是界面键,不能伪装成交易所成交 ID。
后端分别缓存和处理这两路请求。micro_channel 将正常结果、空结果与错误结果转换成不同状态,下面的聚合入口允许一路失败、另一路继续显示:
async def fetch_microstructure(symbol):
depth, trades = await asyncio.gather(
micro_channel(symbol, "depth"),
micro_channel(symbol, "trades"),
)
return {"symbol": symbol, "depth": depth, "trades": trades}
这里使用已导入的 asyncio 和配套后端的 micro_channel。完整 /api/microstructure 入口还检查自选清单与品种能力,并用单品种锁合并并发缓存填充。盘口权限失败只显示该区域的错误代码,不影响报价或成交表;失败结果缓存 60 秒,避免每次页面刷新都立即请求失败的上游。当前文档将盘口和逐笔成交列为免费版不可用能力,不能用免费快照成功推断这两路也有权限。
前端的 useMicrostructure 在一次请求结束后安排下一次,避免慢请求导致定时器不断堆叠。切换品种会取消旧请求、清除定时器,并立即隐藏旧品种数据。两个区域各自显示事件时间或逐笔时间及查询时间,不能拿“查询刚完成”冒充“成交刚发生”。
如果产品要求每笔新成交到达即展示,应继续接入 depth 与 trade WebSocket 频道,注意成交频道名称是单数 trade,REST 路径则是复数 trades。当前文档的港股、美股和加密资产成交推送使用 data.trades 数组,A 股推送把单笔字段直接放在 data 中,适配器必须分别处理。本文已实现的盘口与成交区域使用 REST 轮询,没有将这一扩展路径写成已完成的流式能力。
最近 100 笔可能覆盖几分钟,也可能只覆盖很短的活跃时段。断线后再查最近记录,只能修复可见窗口;完整逐笔采集仍需要明确的序列、补数能力、去重边界和持久化验证。
React 只维护界面状态 不直接持有密钥
浏览器连接本应用的 /ws,收取后端输出的统一数据。对于七个样本,每秒发送整张当前状态表比维护复杂的增量协议更容易验证;状态量增大后,再考虑可见品种订阅、增量帧和虚拟列表。
React 的筛选和搜索是派生结果,不会改变服务端订阅,也不会删除当前选中的详情。界面用真实按钮切换品种,提供键盘焦点、空结果和错误提示;窄屏时列表与详情上下排列。
历史查询需要特别处理请求竞态。用户先点 Apple,再点腾讯,如果 Apple 的慢响应最后到达,不应该覆盖当前腾讯的图表。下面的 Effect 同时取消旧请求并阻止旧回调写入状态:
useEffect(() => {
const controller = new AbortController();
let active = true;
setHistory({ loading: true, data: null, error: null });
fetch(`/api/kline?${params}`, { signal: controller.signal })
.then(async response => {
const body = await response.json();
if (!response.ok) throw new Error(body.detail || "查询失败");
return body;
})
.then(data => {
if (active) setHistory({ loading: false, data, error: null });
})
.catch(error => {
if (active && error.name !== "AbortError") {
setHistory({ loading: false, data: null, error: error.message });
}
});
return () => {
active = false;
controller.abort();
};
}, [params]);
这里 params 是由品种、周期、复权方式生成的稳定查询字符串;setHistory 是组件状态的更新函数。完整示例的 useHistory 还包含重新查询触发参数。切换期间会清空旧图,避免新标题下短暂展示旧品种的曲线。React 官方 Effect 文档同样强调,在清理函数中取消请求或忽略过时结果。React Effect 官方文档
WebSocket Effect 也需要清理:组件卸载时关闭连接并取消重连定时器。否则开发时的重复挂载或页面切换可能留下多条连接。
美股历史数据下载从可解释的查询结果开始
历史下载功能不只是加一个导出按钮。读者需要知道导出的是什么品种、什么周期、哪种复权、何时查询,以及结果是否完整。
示例详情默认请求最近 200 根 K 线,提供 1m、1h、1d 三个页面选项。后端限制代码、周期和返回数量;股票允许 none、forward、backward,其他资产只接受 none。缓存键包含品种、类型、周期、复权方式和数量,不能只使用证券代码,否则日线与分钟线、前复权与不复权会串用。
curl --fail-with-body --get \
'https://api.tickdb.ai/v1/market/kline' \
-H "X-API-Key: ${TICKDB_API_KEY}" \
--data-urlencode 'symbol=AAPL.US' \
--data-urlencode 'type=stock' \
--data-urlencode 'interval=1d' \
--data-urlencode 'adjust=none' \
--data-urlencode 'limit=200'
收到 K 线后,按毫秒时间戳排序、对同一时间戳去重,并检查 low ≤ open/close ≤ high。异常记录不进入图表,页面显示被排除的数量。每根 K 线保留自己的时间戳;图表采用等间距交易条目排列,未返回的时段不额外补蜡烛。因此相邻两根之间的屏幕距离不代表实际经过的时间,具体时刻以横轴和十字光标标签为准。
用 OHLC 数据绘制标准蜡烛图
标准 K 线需要完整的开盘、最高、最低和收盘价。实体两端对应 open 与 close,上下影线延伸到 high 与 low。本例使用 Lightweight Charts 5.2.1,把收盘不低于开盘的 K 线画成红色,下跌画成绿色,并将成交量放进独立副图。颜色比较的是同一根 K 线的开收价格,不是与上一根收盘价比较。
下面是字段映射的核心代码。bars 是后端清洗后的结果,页面需要预先提供 <div id="chart"></div> 容器。接口时间戳是毫秒,图表使用 UTC 秒;只除以 1000,不手动加减时区偏移。
import {
createChart, CandlestickSeries, HistogramSeries,
} from 'lightweight-charts';
const chart = createChart(document.getElementById('chart'), {
height: 400, layout: { attributionLogo: true },
});
const candles = chart.addSeries(CandlestickSeries, {
upColor: '#c84c52', downColor: '#188877', borderVisible: false,
wickUpColor: '#c84c52', wickDownColor: '#188877',
});
candles.setData(bars.map(b => ({
time: b.time / 1000,
open: Number(b.open), high: Number(b.high),
low: Number(b.low), close: Number(b.close),
})));
const volume = chart.addSeries(HistogramSeries, {
priceFormat: { type: 'volume' },
}, 1);
volume.setData(bars.filter(b => b.volume != null).map(b => ({
time: b.time / 1000, value: Number(b.volume),
color: Number(b.close) >= Number(b.open) ? '#c84c5280' : '#18887780',
})));
chart.panes()[1].setHeight(100);
chart.timeScale().fitContent();
完整组件位于 KlineChart.jsx,成交量转换还会排除空字符串、非有限数值和负数;有效的零值照常保留。所有成交量都缺失时,不创建量柱副图,图例显示 —,不会把没有成交量的数据源画成“全天零成交”。为绘图转换出的 JavaScript 数值只用于渲染,CSV 继续使用原始十进制字符串。
图表启用十字光标、滚轮缩放、拖动平移,以及放大、缩小、重置视图按钮。subscribeCrosshairMove 将当前时间映射回原始 K 线,让图例显示对应的开、高、低、收、量。日期标签通过 Intl.DateTimeFormat 按品种时区展示,数据本身保留 UTC 时间;价格轴显示精度不被当作交易所最小报价单位。
React 在 Effect 中创建图表,品种、周期或复权结果变化后装载新数据;清理时取消回调并调用 chart.remove(),同时释放画布和自动尺寸观察器。本文绘制的是当前历史查询结果,没有用 ticker 拼接未经验证的实时 K 线。相关 API 与分窗方式见 Lightweight Charts 官方入门 和 多窗格示例。页面保留 TradingView 的图表署名和链接。

图 3:腾讯控股 200 根真实查询日 K 的运行效果。末根日期为 2026-10-09,开 415、高 425.8、低 414.8、收 424.8,原始成交量 20422500;查询时间为北京时间 2026-10-12 07:02:34。不复权,价格图与成交量图共用时间轴。
当前接口说明允许最后一根 K 线仍在形成,因此页面始终保留提示,CSV 的最后一行标记 last_bar_may_be_open=true。这只是保守提示,不等于服务端已经提供了逐根确认结束标志。不能把最新 ticker 价格随手追加成一根“历史收盘 K 线”。
下载按钮导出当前结果,保留 symbol、interval、adjust、UTC 时间、OHLCV、查询时间和 mode。代码保存的数字字符串不先经浮点重算,界面显示时的四舍五入也不回写到导出数据。合成模式的文件名与内容都带 demo 标识。

图 4:图表和导出使用同一份清洗结果,导出保留查询口径。单次返回的 K 线数量不证明历史区间完整。
如果目标是指定年份的完整美股历史数据下载,应另做一个可恢复的归档任务:先确认账号允许 start_time、end_time,再按有界时间窗口切片;每段记录请求边界、返回首尾、数量和查询时间;相邻片段保留重叠并去重;达到单次上限时继续缩小窗口,直到能够明确验证覆盖。当前文档的 K 线单次数量上限为 1000,免费版不支持指定开始与结束时间。不能用一次 limit=1000 宣称已经下载多年完整历史。
股票历史归档还应记录复权方式和查询时刻:公司行动或数据修订可能改变复权后的历史价格。本文交付的是可浏览、可导出的最近一段结果,没有实现多年归档、交易日完整性审计或总收益回测。
本次实际查询到了什么
北京时间 2026-10-12 07:02–07:03,通过已配置的 TickDB 连接器实际完成 19 次查询:4 次按产品类型分组的报价请求、7 次日 K 请求,以及三地股票和比特币各 1 次盘口、1 次最近成交请求。19 个响应的业务码均为 0。源码中的 app/data/tickdb-capture.json 保存请求参数、开始与结束时间、完整返回内容,不包含 API Key。
报价返回值分别为:贵州茅台 1263 CNY、腾讯控股 424.8 HKD、苹果 336.64 USD、EURUSD 1.11967、XAUUSD 4186.44000、SPX 7811.54000、BTCUSDT 83736.01000000。它们是各自事件时间对应的价格,不能理解为同一时刻的同步报价。
七个品种分别返回 200 根不复权日 K。股票与比特币本次按时间升序返回,外汇、黄金与指数按降序返回,因此进入图表前必须排序。腾讯末根日 K 的收盘价为 424.8,与本次主报价一致;这只是一条样本核对,不能推广为所有接口时刻一致。
盘口结果也有差异:茅台买卖各 5 档、腾讯各 10 档、比特币各 1000 档;页面每侧最多展示 10 档。苹果本次请求成功但买卖数组均为空,页面显示“本次查询未返回记录”,不能填成零价或推断永久不支持。四个品种的最近成交各返回 100 笔,按事件时间倒序展示。外汇、贵金属和指数不在本例盘口与逐笔接入范围内。
capture 模式只开放本次实际取得的日 K、不复权组合;其他周期和复权选项禁用,后端也拒绝未采集的组合。重新读取快照不会更新原始查询时间。要持续请求新行情,需切换下面的 live 模式并在服务端配置密钥。
启动和验证面板
建议使用 Python 3.11 及以上、Node.js 22.12 及以上。先在配套源码的 app 目录安装后端依赖并启动。默认的实查快照模式不需要本地密钥,读取随源码提供的原始响应:
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
APP_MODE=capture uvicorn server:app --host 127.0.0.1 --port 8000
另开终端,在同一 app 目录进入前端工程:
cd frontend
npm ci
npm run dev
打开 http://127.0.0.1:5173。Vite 把 /api 与 /ws 转发给本地 Python 服务。生产构建使用 npm run build;构建完成后重启后端,可通过 http://127.0.0.1:8000 访问静态页面。本文按单进程、单用户本地运行验证,多进程部署需要外置共享行情状态,不能直接复制多份上游连接。
在线模式先在启动服务的终端环境中设置 TICKDB_API_KEY,然后停止原后端并用下面的命令重新启动。没有密钥会拒绝启动,不自动退回合成数据:
APP_MODE=live uvicorn server:app --host 127.0.0.1 --port 8000
后端目前按本地用途绑定回环地址,WebSocket 也限制本地来源。对外提供服务前,需要加入登录、用户权限、数据展示授权、请求配额、反向代理 TLS 和日志脱敏,再调整允许来源。不要只把监听地址改成 0.0.0.0 就把共享密钥支持的行情代理公开出去。
下面是配套 React + Python 程序实际运行的桌面界面,以港股详情展示十档盘口和最近成交。左侧为跨资产自选列表,右侧依次展示当前报价、买卖盘口、逐笔成交和历史走势,底部提供 CSV 下载。

图 5:最终面板的实际运行截图,使用 TickDB 实查保存快照。腾讯报价 424.8 港元,盘口买卖各返回 10 档,成交返回 100 笔,日 K 返回 200 根。各模块保留自己的事件时间与查询时间;截图不代表持续实时推送。
验收时沿着实际操作检查:
- 切换七个资产分类,确认每类样本都可以进入详情,搜索无结果时显示空态。
- 在 AAPL.US 与 700.HK 之间快速切换,确认旧响应不会覆盖当前图表。
- 检查报价、盘口、逐笔成交三个区域;只有一档的响应不补五档,成交价、数量、毫秒时间和方向均来自成交记录。
- 切换外汇、贵金属与指数,确认报价仍可显示,盘口和成交显示能力说明;模拟盘口权限失败,确认成交表及报价继续可用。
- 切换分钟、小时、日线和股票复权,确认请求参数与缓存键同步变化。
- 检查蜡烛实体、上下影线和量柱;用十字光标核对单根 OHLC,验证缩放、重置以及缺失成交量不补零。
- 用缺失值、零值、乱序消息和夜盘消息验证转换,确认空值不是零、旧事件不覆盖新事件、夜盘不覆盖主快照。
- 关闭后端,确认浏览器显示断线且保留旧数据;恢复后端后确认重新连接。旧数据始终保留原事件时间。
- 下载 CSV,检查数据模式、复权、UTC 时间与当前图表一致,最后一根保留可能未结束的提示。
配套验证材料保留了 19 次成功的 TickDB 实际查询响应、请求参数、查询起止时间,以及面板转换与 CSV 对账结果。查询经已配置的 TickDB 连接器完成,本地 Python 默认读取这批快照。本次没有完成本地 Python 到上游 WebSocket 的持续订阅验收,也没有测量全市场覆盖率、持续在线时长或端到端行情延迟。
这套面板让当前报价、盘口档位和逐笔成交拥有各自的数据入口与时间信息,也让每个价格都能回答“属于哪个品种、哪个时段、什么统计窗口、对应什么时间”。扩展更多自选品种时,沿用这些清楚的边界,才能让界面新增的每一列都具有可核对的含义。