免费数据源,无需注册
README 说明数据层使用 baostock 拉取历史与增量日 K,并称其免费、无需注册、不限流。项目不需要你申请 API Key,也不产生数据费用;代价是数据频率只有日线,且依赖该服务的可用性。
Sequoia-X 项目研究站 · 数据层
整套选股系统的地基是一张本地表:stock_daily。数据来自 baostock 的日 K(按后复权口径拉取),日常模式用多进程把当日增量补进来,首次用回填模式把历史一次性灌满。数据库就是一个文件,路径由 DB_PATH 控制,默认落在项目的 data/sequoia_v2.db,可以直接拷贝到另一台机器继续用。
这三个选择决定了它与其他取数方式的不同。
README 说明数据层使用 baostock 拉取历史与增量日 K,并称其免费、无需注册、不限流。项目不需要你申请 API Key,也不产生数据费用;代价是数据频率只有日线,且依赖该服务的可用性。
拉取时使用 adjustflag="1"(后复权)。README 的解释是后复权保持历史价格不变,因此增量写入不会因为后续除权导致既有数据错乱——这是「本地增量存库」这条路能成立的前提。
数据落在单个 SQLite 文件里,意味着同一天的形态判断在本地是确定的、可重复的,也能整库搬迁或备份。README 用「彻底规避东方财富反爬」来描述动机,这属于项目自述的表述,不是技术保证。
建表语句在 data/engine.py 里,字段与 baostock 返回列一一对应。
| 字段 | 类型与约束 | 来源 / 含义 | 注意点 |
|---|---|---|---|
id | INTEGER PRIMARY KEY AUTOINCREMENT | SQLite 自增主键 | 仅用于行标识,策略不使用该列 |
symbol | TEXT NOT NULL | 纯数字股票代码(如 600519、000001) | 入库前会从 baostock 的 sh.600000 形式里去掉市场前缀 |
date | TEXT NOT NULL | 交易日,文本格式 | 日常模式按「最新日期 + 1 天」推算增量起点,格式必须是可解析的日期字符串 |
open / high / low / close | REAL | 后复权的开高低收 | 后复权价与行情软件显示的真实成交价不同,不要直接与看盘软件比数字 |
volume | REAL | 成交量 | 落库前过滤 volume <= 0,停牌与异常行不会进入策略计算 |
turnover | REAL | 成交额;回填路径由 baostock 的 amount 改名而来 | 海龟策略的「成交额过亿」判断用的就是这一列 |
| 约束与索引 | UNIQUE(symbol, date) + 索引 idx_symbol_date(symbol, date) | 保证同股同日不重复,加速按股票查询 | 两条写入路径都依赖它:日常先删后写、回填把冲突静默跳过 |
| 建表方式 | CREATE TABLE IF NOT EXISTS + CREATE INDEX IF NOT EXISTS | 每次启动引擎时执行 | 首次运行会自动创建目录与库文件,不需要手工建表 |
同一个库被两种方式写入,容错策略完全不同。
| 对比项 | 日常增量 sync_today_bulk | 历史回填 backfill | 适用场景 |
|---|---|---|---|
| 并发 | 多进程(最多 8) | 单线程顺序拉取 | 日常求快、回填求稳 |
| 起点 | 每只股票「最新日期 + 1 天」到今天 | 库内为空的用 START_DATE,已有的从断点续拉 | 首次建库用回填,之后用日常 |
| 写入前动作 | 对涉及的每个日期先 DELETE FROM stock_daily WHERE date = ? | 无删除,直接追加 | 日常可安全重跑同一天 |
| 列名映射 | 拉取时直接请求 amount 并命名为 turnover | 先取 amount 再 rename 成 turnover | 两条路最终落到同一列 |
| 冲突处理 | 先删后写,一般不触发约束冲突 | 捕获 sqlite3.IntegrityError 后静默跳过 | 回填重跑不会因重复行中断 |
| 失败重试 | 子进程内单只失败即跳过(记录在返回结果里) | 单只重试 3 次(2s / 4s / 8s),每次重试前重连 | 回填更适合应对长连接超时 |
| 进度可见性 | 汇总后打印写入条数 | 每 500 只打印成功 / 跳过 / 失败计数,结束再汇总 | 回填耗时长,需要进度反馈 |
| 重连策略 | 每个子进程各自 login / logout | 每处理 200 只强制 logout 后重新 login | 规避单连接长时间存活被断开 |
清单来自 baostock 的基础信息接口;展示与链接时会再转一次格式。
| 环节 | 规则 | 适用场景 | 注意点 |
|---|---|---|---|
| 取全市场清单 | bs.query_stock_basic(code_name="", code="") | 回填前确定标的范围 | 接口一次性返回基础信息,需要自己按字段过滤 |
| 过滤条件 | 仅保留 status == "1"(上市)且 stock_type == "1"(股票) | 排除退市与指数等非股票品种 | 这也是「约 5200 只」这个口径的由来 |
| 入库代码 | 取 "sh.600000".split(".")[1] 的纯数字部分 | 库内统一为 6 位数字 | 策略与推送都基于纯数字代码,不带市场前缀 |
| 取数时代码 | 6 / 9 开头 → sh.,其余 → sz. | 把库内代码转回 baostock 格式 | 北交所标的的代码段不在这条规则的显式分支里,实际以你能拉到的数据为准 |
| 推送链接代码 | 6 开头 → SH,4 / 8 开头 → BJ,其余 → SZ | 生成雪球链接(xueqiu.com/S/<code>) | 两套前缀规则不同,读源码时注意区分 |
| 股票名称 | 推送前用 bs.query_stock_basic(code=...) 逐只查名称 | 卡片上显示中文名 | 查不到名称时回退用代码本身,不会中断推送 |
单文件存储带来的便利与要留意的地方。
| 项目 | 说明 | 适用场景 | 注意点 |
|---|---|---|---|
| 默认路径 | data/sequoia_v2.db(相对项目根目录) | 本地开发与定时任务 | 相对路径意味着必须在项目目录下执行,cron 里要显式 cd |
| 改路径 | 改 .env 的 DB_PATH | 想放到数据盘或共享目录 | 父目录会被自动创建,不需要手工建文件夹 |
| 起始日期 | START_DATE 默认 2024-01-01 | 决定回填的历史深度 | 它只影响「库里还没有的股票」的起点;要改历史深度需要清库重填 |
| 备份 | 直接复制该文件(建议在程序未运行时进行) | 迁移到新机器或留档 | 写入过程中复制可能拿到不一致的快照 |
| 体积 | 未给出官方数字;随标的数与历史长度增长 | 容量规划 | 第三方页面给出的「2GB 磁盘」等数字非官方口径,本站不采用 |
| 并发写 | 多进程只并行「拉取」,写库在父进程串行完成 | 避免 SQLite 写锁竞争 | 不要在任务运行时另开进程写同一文件 |
先确认口径,再决定要不要用它做研究。
| 维度 | 实际情况 | 适用场景 | 注意点 |
|---|---|---|---|
| 频率 | 只有日线(frequency="d") | 日频形态扫描 | 没有分钟线与分时数据,日内策略不适用 |
| 复权方式 | 后复权(回填与增量一致) | 跨除权日的区间比较 | 价格数值不等于真实成交价,展示时不要与行情软件混用 |
| 基本面 | 没有任何财务字段 | 纯技术面研究 | 估值、业绩类筛选需要另找数据源 |
| 品种覆盖 | 按基础信息过滤后的 A 股股票 | 个股形态扫描 | 不含 ETF / 基金 / 指数成分(指数只用于对照) |
| 停牌处理 | 过滤 volume <= 0 的行 | 避免停牌日污染形态窗口 | 窗口按实际入库的交易日滚动,与自然日不等长 |
| 数据质量 | 依赖上游数据源;项目未做完整性校验 | 研究用途可接受 | 本站不宣称数据「完全没有缺失」,重要决策前请自行交叉核对 |
这是 A 股数据里最容易踩的一脚:同一个策略、同一段行情,换一种复权口径,信号数就能变。项目在日 K 上固定用后复权,但它在另一个地方用了不复权——两处都要知道。
| 口径 | 定义与序列形态 | 对「每日增量存库」的影响 | 对形态判定的影响 | 本项目用在哪 |
|---|---|---|---|---|
| 后复权 ( adjustflag=1) | 以上市价为基准向后累乘复权因子,历史价格一旦确定就不再变化,序列整体大于现价 | 合适:新数据直接追加,旧数据不用重算 | 涨跌幅连续、除权日不跳空,用于形态与涨跌幅判断更干净 | 全部日 K 数据(回填与日常增量都用它,见 data/engine.py) |
| 前复权 ( adjustflag=2) | 以最新价为基准向前复权,每次除权都要重算整段历史 | 不合适:除权后历史序列变化,本地库会新旧口径混存 | 最新价与现价一致,看图形更直观,但历史值随日期漂移 | 项目未使用 |
| 不复权 ( adjustflag=3) | 原始成交价,除权日出现价格跳空 | 可增量,但缺口需要自己在判定里处理 | 除权日的涨跌幅会出现假暴跌,直接算突破会误判 | 海龟突破计算流通市值时用(_get_market_caps(),因为要用真实价格 × 流通股本) |
成交额 turnover | baostock 的 amount 字段,是当日真实成交金额 | 与复权无关,直接入库 | 本身就与复权无关,所以「成交额过亿」门槛不受口径影响 | 海龟突破的 1 亿门槛 |
成交量 volume | 股数,不受价格复权影响;但送转后单位会变 | 直接入库 | 放量倍数(1.5 倍 / 2 倍)跨除权日比较时会被送股放大 | 均线放量、涨停洗盘、跌停策略的放量判定 |
| 流通市值(派生) | 不复权收盘价 × 流通股本,股本由 volume ÷ (turn × 100) 反推 | 不入库,每次命中后即时查询 | 用于命中结果的排序,不参与选股条件 | 海龟突破命中后的排序(按流通市值从大到小) |
「今天没结果」有两种可能:市场没有符合条件的票,或者你的库本身有问题。下面五条查询按顺序跑一遍,五分钟内能分清是哪种。
SELECT MAX(date) AS latest, COUNT(DISTINCT date) AS days,
COUNT(DISTINCT symbol) AS symbols, COUNT(*) AS rows
FROM stock_daily;
四个数字里,latest 必须等于你要回测/核查的那个交易日。非交易日运行不会新增行,所以它通常停在上一个交易日——这是预期行为,不是故障。
SELECT symbol, date, COUNT(*) c
FROM stock_daily GROUP BY symbol, date HAVING c > 1 LIMIT 20;
正常应当返回空。表上建的是 UNIQUE (symbol, date),回填路径的重复写入会被 sqlite3.IntegrityError 捕获后静默跳过(except sqlite3.IntegrityError: pass)——也就是说重复不会入库,但也不会留下日志。真出现重复行说明表结构被手工改过。
-- 每只股票的起止日期与 K 线根数(根数明显少于日历天数的就是长期停牌或样本不足)
SELECT symbol, MIN(date) AS first, MAX(date) AS last, COUNT(*) AS bars
FROM stock_daily GROUP BY symbol
ORDER BY bars ASC LIMIT 20;
日增量在入库前会过滤 volume = 0 的行,停牌日因此不会写进库。后果是「最后一根 K 线」不一定是「今天」,策略读到的「今日」可能是一个月前——排查零命中时这是第一顺位嫌疑。
-- 统计各阈值下的可参与标的数
SELECT
SUM(bars >= 3) AS ge3, -- 涨停洗盘
SUM(bars >= 21) AS ge21, -- 海龟突破
SUM(bars >= 40) AS ge40, -- 高窄旗形
SUM(bars >= 60) AS ge60 -- 上升趋势跌停
FROM (SELECT symbol, COUNT(*) bars FROM stock_daily GROUP BY symbol);
四个数字差得越多,说明回填起始日期(默认 2024-01-01)对新股覆盖越不均匀。K 线不足的标的会被静默跳过,不会报错。
SELECT date, open, close, volume, turnover FROM stock_daily
WHERE symbol = '600519' ORDER BY date DESC LIMIT 5;
把 close 与行情软件的前复权/不复权价对照:后复权价通常更高(含历史分红送股)。对不上不代表数据错,先确认你比较的是哪一种口径。
data/engine.py 为准,任何口径冲突以官方源码为最终依据。库里的「全市场」并不是你想象中的全市场。下面这些标的从一开始就不会出现在 stock_daily 里,而它们恰好是最容易让回测结论偏乐观的一批。
| 标的类型 | 源码依据 | 后果 | 你怎么处理 |
|---|---|---|---|
| 已退市 / 已暂停上市股票 | get_all_symbols() 只保留 status == "1"(上市)且 stock_type == "1"(股票)的记录 | 幸存者偏差:历史上退市的票永远不在库里,「一直持有」类结论天然偏乐观 | 若结论涉及长期持有或胜率,必须显式声明这一点,或另外补入已退市样本 |
| 北交所早期代码(4xxxxx / 8xxxxx) | _to_baostock_code() 只判「6 或 9 开头 → sh,其余 → sz」,没有北交所分支;而推送模块 _to_xueqiu_code() 却把 4/8 开头识别为 BJ | 取数侧会把 4/8 开头的代码拼成 sz.xxxxxx,baostock 查不到 → 该股票长期无数据、被跳过 | 要覆盖北交所需自行改 _to_baostock_code() 的前缀判定并重跑回填;改动前先确认目标代码段的真实前缀 |
| ST / *ST 股票 | 基础信息筛选只看上市状态与证券类型,不排除 ST | ST 股会进库并参与选股,但涨跌幅上限是 5%,而策略用固定 9.5% 系数判断涨停/跌停 → 会被系统性漏掉 | 知道自己拿到的是「含 ST 的股票池」,且 ST 天然不满足 1.095 / 0.905 判定 |
| 指数、ETF、基金、可转债 | stock_type == "1" 只保留股票类,其他证券类型被过滤 | 库里没有指数数据 —— 这意味着无法直接算「相对沪深 300 的超额」 | 基准数据要另外取;用本项目库做回测时,「相对收益」只能自己补 |
| 上市不足 N 个交易日的次新股 | 各策略有 _MIN_BARS(3 / 21 / 40 / 60)硬门槛,不足直接 continue | 样本不足的标的被静默跳过,不会出现在日志里 | 把回填起始日期提前,让更多标的攒够 K 线;或接受「新股不参与」这一事实 |
| B 股(900xxx / 200xxx) | 同样受「6 或 9 开头 → sh,其余 → sz」规则影响,且基础信息筛选口径未专门区分 | 代码前缀可能与真实市场不符,导致取数失败或无数据 | 与北交所同因:先确认代码段,再决定是否改代码映射 |
「后复权」三个字最容易被误用。本站用同一只票、同一区间,分别以 adjustflag="1"(后复权)和 adjustflag="3"(不复权)各拉一次真实数据,读数如下。
| 对照项(600519,2025-09-05 ~ 2026-10-09) | 后复权 adjustflag=1 | 不复权 adjustflag=3 | 差异 |
|---|---|---|---|
| 区间行数 | 261 | 261 | 行数一致,说明两条口径只改价格、不改交易日 |
| 区间首日 close | 约 7.37 倍于不复权 | 基准 | 比值 7.3665 |
| 最近一日(2026-10-09)close | 9686.27 | 1263.00 | 比值 7.6693 |
| 比值是否恒定 | 不恒定:区间首日 7.3665 → 最近一日 7.6693 | 说明复权因子随时间累积,不能用一个固定倍数换算回去 | |
| 成交额(amount) | 4453223189.01 | 4453223189.01 | 金额字段不受复权影响,两条口径完全一致 |
import baostock as bs
bs.login()
for flag, label in [("1", "hfq"), ("3", "raw")]:
rs = bs.query_history_k_data_plus("sh.600519", "date,close,amount",
start_date="2025-09-05", end_date="2026-10-09", frequency="d", adjustflag=flag)
rows = []
while rs.next():
rows.append(rs.get_row_data())
print(label, rows[0], rows[-1])
bs.logout()
预期输出:两个口径的 amount 相同、close 差 7 倍以上,且比值在两个端点上不一样。
| 用途 | 能不能用库里的价 | 原因 |
|---|---|---|
| 算形态、算均线、算突破(策略内部) | 可以 | 同一标的内部自洽,复权因子在序列里被吸收掉,形态比例正确 |
| 算区间收益率 | 可以 | 分子分母同口径,除权跳空被还原 |
| 当「我应该出多少钱买」的成交价 | 不行 | 9686.27 不是市场价,真实盘口在 1263 附近;按它下单、算持仓、算费用全错 |
| 判断涨跌停、算一手多少钱 | 不行 | 涨跌停按不复权价与前收比,后复权序列里的「涨停」比例不是 10% |
| 算手续费、印花税、滑点 | 不行 | 这些成本按真实成交金额计,后复权价会把成本放大到 7 倍以上 |
| 和行情软件截图对照 | 对不上是正常的 | 软件默认显示不复权(或前复权),口径不同不必怀疑数据错 |
adjustflag="1",而海龟策略算流通市值时用 adjustflag="3" 取真实价。也就是说库里的价只适合做形态与收益,任何涉及「成交」的计算都要另取不复权价。这是源码事实,不是本站推断。这一节是本轮最重的一条。第 3 张表(两条写入路径)已经写了「日常模式按日期先删后写」,但它到底有多危险,只有跑出来才知道。下面是受控复现的读数。
复制一份真实库 → 把其中一只标的(600000)的日期截断到 2026-06-30(模拟「长期停牌 / 连续抓取失败导致落后」)→ 调用项目原生的 engine.sync_today_bulk()。被截断的那只标的因此有了 3 个月的追赶窗口,其余 264 只都是最新状态,窗口只有 1 天且取不到数据。
# 复现脚本:seo_reports/sequoia-x/_run/verify_delete_scope.py
DELETE FROM stock_daily WHERE symbol='600000' AND date > '2026-06-30'; -- 67 行
written = DataEngine(settings).sync_today_bulk() # 返回 67| 度量项 | 调用前 | 调用后 | 变化 |
|---|---|---|---|
| 全库行数 | 176,646 | 159,036 | 少 17,610 行(9.97%) |
| 受影响日期数(2026-07-01 起) | 67 个交易日 | 67 个交易日 | 日期还在,但内容被换掉了 |
| 每个受影响日期的覆盖标的数 | 264 只 | 1 只 | 只剩被追赶的那一只 |
sync_today_bulk() 返回值 | — | 67 | 返回值只报告「我写入了 67 行」,不报告删掉了 17,610 行 |
| 耗时 | — | 17.4 秒 | 整个过程没有任何一处报错或告警日志 |
with sqlite3.connect(self.db_path) as conn:
for d in df["date"].unique().tolist():
conn.execute("DELETE FROM stock_daily WHERE date = ?", (d,)) # ← 只带日期,不带 symbol
df.to_sql("stock_daily", conn, if_exists="append", ...) # ← 只写回本次取到的那些标的
conn.commit()
删除的粒度是「日期」,写回的粒度是「本次真正取到新数据的标的」。只要这两者的集合不相等,差值就是被删掉且没有补回来的历史。触发条件不止「标的落后」一种:
| 触发场景 | 为什么会出现 | 后果量级 |
|---|---|---|
| 某只标的长期落后(停牌、退市边缘、连续抓取失败) | 它的追赶窗口 = 从最后一次入库到今天,可能长达数月 | 窗口内所有标的、所有日期的行被删 |
| 同一天重复执行日常模式 | 先跑成功的标的会被跳过(last_date >= today),只有失败的那些还留在队列里 | 第二次运行把当天已写好的全市场行删掉,只写回少数几只 |
| 节假日/周末连续触发 | 非交易日取不到数据,通常直接返回(安全);但只要有任意一只标的能取到数据,就会进入删除分支 | 删除范围等于那只标的的追赶窗口 |
| 动作 | 具体做法 | 效果 |
|---|---|---|
| 每天跑之前先备份库 | 把 data/sequoia_v2.db 复制一份带日期的副本(68 MB 级别,几秒钟) | 出事能整库回滚,这是成本最低的一条 |
| 别重复触发 | 任务计划程序设「已在运行时不再启动新实例」;cron 不要写成每分钟 | 掐掉「同一天第二次运行」这个高频触发点 |
| 让标的别落后 | 日常运行后查 SELECT symbol, MAX(date) m FROM stock_daily GROUP BY symbol HAVING m < (SELECT MAX(date) FROM stock_daily);,落后就单独补 | 把追赶窗口压回 1 天,删除范围就只剩 1 天 |
| 改代码(治本) | 把 DELETE ... WHERE date = ? 改成同时带 symbol 条件(或按 (symbol,date) 做 upsert) | 写入范围与删除范围一致,问题根除;属于改动项目源码,需自行评估 |
| 发现掉数据后怎么恢复 | 先停掉定时任务 → 用备份库覆盖 → 重跑回填(已入库会跳过)与日常模式 | 不要直接对着被删过的库继续跑,否则会把残缺状态当成基线 |
UNIQUE 约束兜住重复,已入库会跳过);增量在特定条件下会把历史「连片删空」,而且不报错。以官方源码为最终依据,本站只负责把实测现象与复现脚本摆出来。「最后一根 K 线」到底是不是今天、是不是交易日,是排查零命中时最先要确认的事。下面这套检查来自本机 789 只标的、524,104 行、668 个交易日的真实库。
SELECT MAX(date) AS latest, COUNT(DISTINCT date) AS days,
COUNT(DISTINCT symbol) AS symbols, COUNT(*) AS rows
FROM stock_daily;
-- 本站读数:latest=2026-10-09, days=668, symbols=789, rows=524104
判定规则很硬:如果今天是交易日、而且你已经跑过日常模式,latest 必须等于今天;否则不是数据源没出数,就是你的更新没成功。反过来说,latest 停在上一个交易日恰恰是非交易日的正常表现。
SELECT date, CAST(julianday(date) - julianday(LAG(date) OVER (ORDER BY date)) AS INT) AS gap_days
FROM (SELECT DISTINCT date FROM stock_daily) ORDER BY date DESC LIMIT 12;
本站 668 个交易日、667 个间隔的真实分布:
| 间隔(自然日) | 出现次数 | 含义 |
|---|---|---|
| 1 天 | 524 | 正常周中交易日 |
| 2 天 | 1 | 极少见(临时休市等) |
| 3 天 | 124 | 周五 → 周一,信号日与可成交日天然差 3 个自然日 |
| 4~9 天 | 16 | 长假(清明/五一/端午/中秋等) |
| 11 天 | 2 | 国庆长假 |
合计 142 / 667(21.3%)的相邻交易日间隔 ≥ 3 天。所以「收盘后出信号、第二天开盘买」这句话,在长假前后实际是「等 4~11 个自然日」,任何回测都必须按交易日而不是自然日推进。
>>> bs.query_trade_dates(start_date="2026-09-28", end_date="2026-10-10")
error_code: 10001011 error_msg: 黑名单用户,请与管理员联系 rows: 0
本站回填过程中触发了 baostock 的访问限制(详见安装页的速率实测),连交易日历接口一起被拒。所以本轮没有完成「官方交易日历 vs 库内日期」的逐日差集核对——这一项标注为未实测,不编造结论。可用的替代口径有两个:①用 SELECT DISTINCT date FROM stock_daily 作为「实际发生的交易日」基准(多标的取交集更稳);②把交易日历文件另行落盘后与库内日期做集合差。等数据源恢复后,本项应重跑并回填结论。
SELECT DISTINCT date FROM stock_daily 可以直接当交易日历用——前提是所有标的都回填过、且没有标的落后。本轮 789 只全部从 2024-01-02 连续到 2026-10-09,668 个日期可以放心使用;判断依据以官方源码与 baostock 返回为准。源码里拉取参数是 adjustflag="1"(后复权)。README 给出的理由是后复权下历史价格不变,增量写入时旧的记录不需要重算,适合这套「每天只补增量」的存储方式。口径细节以官方 README 与 baostock 文档为准。
因为库里存的是后复权价,行情软件默认多显示不复权价。两者在除权除息后会分叉,这是设计使然而不是数据错误。要做展示或对比时,请注明复权口径。
表结构是公开的,理论上可以手工插入或另写脚本导入,但要注意两点:UNIQUE(symbol, date) 约束会拒绝重复行;以及策略假定 date 是可排序解析的日期字符串。自行写入导致的结果差异由你自己承担,本站不做保证。
写库动作都在单个进程里串行执行,并发只用在「拉取」阶段;日常模式按日期先删后写,回填模式靠 UNIQUE 约束把重复行静默跳过。因此不要在同一时间同时跑两个模式,正常使用下二者是互补的。
高窄旗形与上升趋势跌停这两个策略会直接跳过 K 线不足的标的。解决办法是把 START_DATE 往前提(例如更早的年份)并重跑回填;默认值是 2024-01-01,历史深度越深,能参与计算的标的越多,但回填耗时也越长。
可以。它是单个 SQLite 文件,迁移后把 DB_PATH 指向新位置即可继续日常更新。注意目录相对路径的基准是「执行命令时所在的目录」,在定时任务里要显式切到项目目录。
按四个可能依次排查:①它是否已退市或暂停上市(get_all_symbols() 只取 status=1 的记录,退市股不进库);②代码前缀是否被正确映射(源码只判 6/9 开头为 sh,北交所 4/8 开头会被误拼成 sz 而取不到数据);③证券类型是否为股票(指数、ETF、基金被 stock_type=1 过滤);④回填是否在它上市之前就结束了。判断依据以官方源码与 baostock 返回为准。
用本页第 3 条查询列出每只股票的 MIN(date)、MAX(date) 与 K 线根数,把根数与区间内的交易日数量对比即可。停牌日因为 volume = 0 在入库前被过滤,所以不会留下记录——断层是「静默」的,只能靠比对发现。任何策略在取值前都应先断言库内最新日期等于目标交易日,以官方源码口径为准。