股票 API 怎么用?报价展示与历史研究,为什么要分开处理
TickDB Research ·
摘要:面向正在交付股票行情页和历史导出功能的金融产品经理,本文拆解报价快照、K 线与研究文件的区别,解释相同价格为何可能表达不同信息,以及行情时间、取数日期和研究截止日各自解决什么问题。结合 TickDB 保存的真实响应和可运行 Python 示例,生成带时间的静态报价卡片与按日期筛选的研究 CSV,帮助产品和技术同事用明确规则检查交付。
假设你正在做一个股票详情页:上方显示报价,下方支持导出历史数据。接口已经接通,页面有价格,CSV 也能下载。但用户还会问:这个价格截至什么时候?导出的最后一天包含哪些信息?为什么页面价格与研究表最后一行不同?这些问题没有讲清,两个功能即使都能运行,用户仍可能用错数据。
处理这类需求,可以先分清两类输入:报价快照用于展示查询取得的行情状态,历史 K 线用于按周期组织观察和研究记录。本例使用 TickDB 的报价与历史 K 线查询结果,再由应用分别生成卡片和研究表。两条路径围绕同一只股票展开,各自保留用途所需的时间和口径。
一、先分清:报价、K 线和 CSV 分别是什么
报价快照:查询取得的行情状态
行情页上的“最新价”,通常需要与证券和行情时间一起理解。比如同样显示 880,旁边写“10 月 30 日 15:00:02”,与没有时间的 880,读者能作出的判断就不同。
报价快照也可能包含开盘价、最高价、成交量等统计字段,不能简单理解为“只有一个价格”。但一次快照不会自动提供一段完整的逐周期历史序列。使用 TickDB 时,可以从报价响应中的 symbol、last_price 和 timestamp 识别证券、最新成交价和返回的数据时间;其他字段的含义与返回条件见行情快照文档。
K 线:指定周期内的统计记录
一根交易价格 K 线,把某个周期内的成交价格整理为开盘、最高、最低、收盘等信息。把多根 K 线按时间排列,才得到对应周期的历史序列。
因此,使用历史数据之前,至少要知道:是哪只股票、什么周期、采用什么复权口径,以及各条记录对应哪个时间区间。日线与分钟线表达的区间不同,不能只按字段都叫 close 就互相替代。
CSV:应用整理后的文件
CSV 是保存表格的一种格式。把最新价写入 CSV,保存下来的仍然是一条报价;把经过筛选的日线写进去,得到的是按条件整理的历史记录。
文件格式决定怎样保存,数据含义决定怎样使用。HTML、JSON、CSV 可以承载结果,但不能替代对价格、周期和时间的解释。

图 1:AI 解释插图。报价展示与历史研究按用途分别处理。TickDB 提供本例的行情输入,卡片排版与 CSV 导出由应用实现。
二、为什么分开处理?两个容易混淆的底层问题
价格相同,表达的信息仍可能不同
用一个教学构造来说明。假设 10:00:00 至 10:01:00 这个区间内只有三笔成交:
| 成交时间 | 成交价 |
|---|---|
| 10:00:05 | 100 |
| 10:00:20 | 102 |
| 10:00:50 | 101 |
这一分钟结束后,开盘价是 100,最高价是 102,最低价是 100,收盘价是 101。最后一笔成交价也恰好是 101。
这两个 101 数字相同,一个描述最后一笔成交,一个描述这个指定区间的收盘。只保留 101,无法知道这一分钟曾到过 102,也无法还原区间内的成交过程。即便保留开高低收,也没有保留全部逐笔路径。
所以,行情展示可以围绕最新成交价及其时间展开;需要比较多个周期的研究,则应加载相应历史序列,并保持周期和价格口径一致。具体任务决定保留哪些信息。
记录标注的时间,与信息可用的时间要分开理解
继续看上面的三笔成交。如果程序在 10:00:30 做判断,10:00:50 那笔 101 还没有发生。当时已发生的最后一笔成交是 102,这一分钟最终以 101 收盘,尚不能被当作已知事实。

图 2:AI 解释插图,数值为教学构造,非 TickDB 实测。假设程序在判断前已收到前两笔成交;真实系统还需考虑数据发布与接收的延迟。
历史文件里,一根 K 线可能用区间起点标注,也可能采用其他时间标签。读取时需要确认时间定义,不能看到标签就把整根 K 线的最终值放进该时刻的判断。QuantConnect 的时间建模文档也区分了周期起止与数据交付时间,以避免提前使用尚未形成的信息。时间建模说明。
对股票日线,还要留意复权。复权用于处理公司行动对历史价格比较的影响;以后重新查询时,历史复权数值可能变化。TickDB 的 K 线说明指出,末根 K 线可能仍在形成,复权历史可能随公司行动或数据更新而调整。K 线使用说明。
这里有三个不同的问题:记录属于哪一天?周期是否已经结束?现在下载到的历史版本,当时是否可取得?仅靠筛选日期,只处理了第一个问题。
三、怎样落实?先给两个功能各写一句使用规则
对于这个股票详情页,可以把需求写成:
报价展示:显示指定股票的一条保存报价,同时展示行情时间、取数日期与更新状态。
研究导出:导出指定股票、指定周期与复权口径的记录,按约定时区保留截止日及之前的数据,并记录实际输出范围。
行情时间回答“价格对应哪个时点”,取数日期回答“什么时候取得这份输入”,研究截止日回答“这次输出保留到哪里”。三个条件分别参与判断,不应相互代替。
例如,11 月 1 日取得一份行情响应,其中的报价时间仍可能是 10 月 30 日。页面如果把请求当天直接写成行情日期,用户就可能把保存的收盘后快照理解为刚发生的新成交。
对于研究导出,产品经理也需要明确“截止到某一天”是按什么日期规则处理。本例只处理 A 股日线,使用北京时间的日历日期筛选;跨市场或分钟线任务应重新定义周期和交易日期。
四、用 TickDB 保存的真实输入生成两份结果
本例的原始响应取得于 2026 年 11 月 1 日,证券是寒武纪 688256.SH。其中:
- 报价
last_price原值为880,数据时间换算为北京时间是2026-10-30 15:00:02。 - 历史输入包含 10 月 26 日至 30 日的五条日线,周期为
1d,复权方式为forward,即前复权。
示例没有重新取当前行情。我们用这组保存输入,分别制作静态报价卡片和日期筛选导出。
报价卡片保留 880,并同时保留数据时间和 11 月 1 日的取数日期;卡片标明它是保存快照,不持续更新。
研究表另设演示截止日 2026 年 10 月 29 日。程序将五条输入中的 10 月 30 日记录排除,留下四行:
| 日期 | 前复权收盘价原值 |
|---|---|
| 2026-10-26 | 968 |
| 2026-10-27 | 912.98 |
| 2026-10-28 | 958 |
| 2026-10-29 | 913 |
因此,报价卡片中是 880,研究表最后一行是 913。本例可以沿日期条件追查差异:两份输出采用了不同的记录范围。实际项目遇到价格差异,还应核对周期、复权和更新状态,不能仅凭数值判断对错。
这里使用 TickDB 的两类查询,是因为同一个功能页同时需要报价状态和历史序列。报价响应保留证券与行情时间,K 线响应保留证券、周期、复权和逐条记录,便于应用按对应条件检查与整理。接入入口见股票 API 专题页。
五、用 Python 复现,再核对结果
文末脚本使用 Python 3.9+ 标准库。默认内置本例保存响应的必要字段,不需要密钥和网络即可复现本地处理。将代码保存为 prepare_stock_outputs.py,运行:
python3 prepare_stock_outputs.py --out stock_outputs
它会生成四个文件:
| 文件 | 用途 |
|---|---|
quote_card.html | 浏览器打开静态报价卡片 |
quote_card.json | 保留卡片使用的结构化记录 |
research.csv | 保存筛选后的四条日线记录,含证券、时间戳、周期、复权和收盘价 |
processing.json | 保存截止日、输入条数、保留范围和未检查事项 |
2026 年 11 月 3 日保存的运行结果为:
QUOTE 688256.SH price=880 data_time=2026-10-30T15:00:02+08:00
RESEARCH input=5 kept=4 excluded=1 cutoff=2026-10-29
LAST date=2026-10-29 close=913 adjust=forward
FILES quote_card.html quote_card.json research.csv processing.json
BOUNDARY saved-data processing; no network requests; calendar/finalization/PIT not checked
可以先检查卡片上的行情时间,再核对 CSV 的全部四行及处理说明:输入五行、保留四行、排除一行,所有输出日期均不晚于截止日。最后一行是 10 月 29 日,只是其中一项检查。
脚本按原始字符串写入价格,CSV 使用带 BOM 的 UTF-8 编码。Excel、WPS 可能自动改变显示格式,复查时保留源文件。重复运行应改用新输出目录,例如 --out stock_outputs_run2,程序会拒绝写入已有目录。
使用自己的保存响应
取得自己的 TickDB 报价和日线业务响应后,将完整 JSON 分别保存为 ticker.body.json 与 kline.body.json,保留返回结构。对应本文样本的命令如下:
python3 prepare_stock_outputs.py --ticker ticker.body.json --kline kline.body.json --retrieved-on 2026-11-01 --cutoff 2026-10-29 --symbol 688256.SH --adjust forward --out stock_outputs_from_files
换用新数据时,按实际输入调整证券、取数日期、截止日和复权参数。这个处理器要求匹配的报价与 A 股日线结构,并固定使用北京时间日期;并非改几个参数就能直接覆盖其他市场和所有周期。
写文件前,程序核对业务状态、证券、日线周期、复权设置、重复日期和价格。已有运行记录显示,证券不匹配、复权不匹配、截止日前无记录,以及输出目录已存在时,程序均停止。
六、进入真实产品或研究之前,还要补哪一步
这份示例完成了静态展示和日期筛选。接入真实功能时,下一步由用途决定:
- 行情页需要持续更新,就进一步实现更新机制、记录接收时间,并设计断线与过期提示;静态 HTML 本身不会订阅行情。
- 历史数据准备进入回测,就继续核对交易日覆盖、周期是否完成,以及复权历史版本的可用性。把后来下载的数据筛到过去某天,没有恢复当时的数据版本。
可以先用自己准备支持的一只股票,写清“展示哪个时点的报价、导出哪个口径和范围”,再交回卡片、研究表、处理条件与原始输入。用户看到的价格,以及技术同事保存的记录,就有了可以共同核对的依据。
数据来源:TickDB,原响应取得于 2026 年 11 月 1 日;处理程序于 2026 年 11 月 3 日本地运行。文中三笔成交为教学构造,证券与历史价格用于数据整理演示,不构成投资建议。
附录:完整 Python 代码
import argparse
import csv
import html
import json
from datetime import date, datetime, timedelta, timezone
from decimal import Decimal
from pathlib import Path
SHANGHAI = timezone(timedelta(hours=8), name="Asia/Shanghai")
# Fields extracted from TickDB responses retrieved on 2026-11-01.
SAVED_QUOTE = {"symbol": "688256.SH", "last_price": "880", "timestamp": 1793343602000}
SAVED_BARS = [
{"time": 1792944000000, "close": "968"},
{"time": 1793030400000, "close": "912.98"},
{"time": 1793116800000, "close": "958"},
{"time": 1793203200000, "close": "913"},
{"time": 1793289600000, "close": "880"},
]
def local_time(value):
if type(value) is not int or value < 0:
raise ValueError("Expected a nonnegative integer Unix timestamp in milliseconds")
return datetime.fromtimestamp(value / 1000, SHANGHAI)
def price(value):
if not isinstance(value, str):
raise ValueError("Expected an original price string")
number = Decimal(value)
if not number.is_finite() or number <= 0:
raise ValueError("Expected a finite positive price")
return value # Preserve the original string.
def main():
p = argparse.ArgumentParser(description="Prepare saved China A-share quote/daily outputs")
p.add_argument("--ticker", type=Path)
p.add_argument("--kline", type=Path)
p.add_argument("--retrieved-on", help="Actual retrieval date, YYYY-MM-DD")
p.add_argument("--cutoff", help="Inclusive Asia/Shanghai calendar date")
p.add_argument("--symbol", default="688256.SH")
p.add_argument("--adjust", choices=["none", "forward", "backward"], default="forward")
p.add_argument("--out", type=Path, default=Path("stock_outputs"))
a = p.parse_args()
custom = a.ticker is not None or a.kline is not None
if custom:
if not all([a.ticker, a.kline, a.retrieved_on, a.cutoff]):
raise ValueError("Custom mode requires --ticker, --kline, --retrieved-on and --cutoff")
ticker = json.loads(a.ticker.read_text(encoding="utf-8-sig"))
kline = json.loads(a.kline.read_text(encoding="utf-8-sig"))
if ticker.get("code") != 0 or kline.get("code") != 0:
raise ValueError("API response was not successful; inspect code/message")
matches = [q for q in ticker["data"] if q.get("symbol") == a.symbol]
if len(matches) != 1:
raise ValueError("Expected exactly one quote for the requested symbol")
quote, data = matches[0], kline["data"]
retrieved_on, source_mode = a.retrieved_on, "saved_response_files"
else:
if a.retrieved_on:
raise ValueError("Embedded sample retrieval date is fixed at 2026-11-01")
quote = SAVED_QUOTE
data = {"symbol": "688256.SH", "interval": "1d", "adjust": "forward", "klines": SAVED_BARS}
retrieved_on, source_mode = "2026-11-01", "embedded_saved_field_extract"
date.fromisoformat(retrieved_on)
cutoff = date.fromisoformat(a.cutoff or "2026-10-29")
if (quote["symbol"], data["symbol"], data["interval"], data["adjust"]) != (a.symbol, a.symbol, "1d", a.adjust):
raise ValueError("Symbol, daily interval or adjustment does not match settings")
quote_price = price(quote["last_price"])
quote_time = local_time(quote["timestamp"]).isoformat()
rows = sorted(data["klines"], key=lambda row: local_time(row["time"]))
if not rows:
raise ValueError("No daily bars")
stamps = [row["time"] for row in rows]
dates = [local_time(ts).date() for ts in stamps]
if len(set(stamps)) != len(stamps) or len(set(dates)) != len(dates):
raise ValueError("Duplicate daily timestamps or dates")
for row in rows:
price(row["close"])
kept = [row for row in rows if local_time(row["time"]).date() <= cutoff]
if not kept:
raise ValueError("No daily rows at or before cutoff")
card = dict(symbol=a.symbol, price_original=quote_price,
data_time=quote_time, retrieved_on=retrieved_on,
view="saved snapshot; no continuous updates")
meta = dict(symbol=a.symbol, interval="1d", adjust=a.adjust,
date_basis="Asia/Shanghai calendar date", cutoff=cutoff.isoformat(),
retrieved_on=retrieved_on, source_mode=source_mode,
input_rows=len(rows), kept_rows=len(kept), excluded_rows=len(rows)-len(kept),
first_date=local_time(kept[0]["time"]).date().isoformat(),
last_date=local_time(kept[-1]["time"]).date().isoformat(),
last_close_original=kept[-1]["close"],
not_checked=["trading-calendar completeness", "bar finalization", "point-in-time adjusted history"])
a.out.mkdir(parents=True, exist_ok=False)
(a.out / "quote_card.json").write_text(json.dumps(card, ensure_ascii=False, indent=2), encoding="utf-8")
fields = "symbol,date_asia_shanghai,time_ms,interval,adjust,close_original".split(",")
with (a.out / "research.csv").open("w", newline="", encoding="utf-8-sig") as f:
w = csv.writer(f)
w.writerow(fields)
for row in kept:
w.writerow([a.symbol, local_time(row["time"]).date().isoformat(), row["time"], "1d", a.adjust, row["close"]])
(a.out / "processing.json").write_text(json.dumps(meta, ensure_ascii=False, indent=2), encoding="utf-8")
content = "".join(f"<dt>{html.escape(str(k))}</dt><dd>{html.escape(str(v))}</dd>" for k, v in card.items())
(a.out / "quote_card.html").write_text(
'<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">'
'<title>Saved stock quote</title><style>body{font:18px system-ui;max-width:600px;margin:40px auto;padding:20px;background:#f7f8fa}'
'main{background:white;padding:28px;border:1px solid #dce2ea;border-radius:12px}dt{color:#536070;margin-top:18px}dd{margin:6px 0;overflow-wrap:anywhere}</style>'
f'<main><h1>Saved stock quote</h1><dl>{content}</dl><p>Source: TickDB. Static saved-data example.</p></main></html>', encoding="utf-8")
print(f"QUOTE {a.symbol} price={quote_price} data_time={quote_time}")
print(f"RESEARCH input={len(rows)} kept={len(kept)} excluded={len(rows)-len(kept)} cutoff={cutoff}")
print(f"LAST date={meta['last_date']} close={meta['last_close_original']} adjust={a.adjust}")
print("FILES quote_card.html quote_card.json research.csv processing.json")
print("BOUNDARY saved-data processing; no network requests; calendar/finalization/PIT not checked")
if __name__ == "__main__":
try:
main()
except (ValueError, KeyError, TypeError, OSError, ArithmeticError, OverflowError) as exc:
raise SystemExit(f"ERROR: {exc}")