常见问题
数据源相关
东财返回 403 怎么办?
系统会自动处理:
- 403 触发
RateLimitError(不重试)并记录熔断器失败 - 连续 4 次失败后熔断器开启,跳过东财 120 秒
- 自动降级到后续数据源(TDX / 腾讯 / 新浪备用)或 DB 历史数据
- 120 秒后自动探测是否解封
无需人工干预。如果持续 403,说明 IP 被东财风控,等待数小时后自动恢复。
为什么有些数据显示为 0?
可能原因:
- 东财返回
"-"表示无数据,经safe_float()转换为0.0 - 非交易时段部分实时数据不可用
- 数据源全部失败,返回空结构
- DB 历史快照已超出陈旧窗口(
_snapshot_stale:最近交易日 − 3 天),系统按空数据处理而不冒充当日行情
Baostock 连接失败?
本项目已完全移除 baostock 第三方依赖:data_sources/baostock_client.py / baostock_provider.py 是保留类名的自建兼容壳,内部改走东财 push2his(历史 K 线)、financial_deep(财务)与 fox_stock_master(股票基础信息),不再有 bs.login() 环节。
若这些接口报错,排查方向是:
- 东财是否触发风控(看日志中的 403/429)
- 本机数据库是否可用(
fox_stock_master读取需要 DB) - 网络出口是否通畅
如何判断当前数据来自哪个源?
载荷级 _source 只记录"这次请求的主源",而一次响应常由多个源拼成
(如 sentiment 的指数段与涨跌家数段来自不同源)。对账/排查口径问题必须看段级标签:
| 段级标签 | 出处 |
|---|---|
_indices_source | 指数段 |
_breadth_source | 涨跌家数段 |
_limit_source | 涨跌停家数段 |
_bins_source | 涨跌幅分布分桶段 |
_sectors_source | 行业排名段 |
_cells_source | 板块热力图格子段 |
段级标签随 fox_market_snapshot.data_json 一起落库,出处随值改写;hot_stocks
是例外——它由首行提升为载荷级 source / source_label / as_of,不再另打段级标签。
性能相关
为什么首次加载较慢?
首次请求需要:
- 建立 TCP 连接(TDX 服务器探测)
- 初始化 HTTP 连接池
- 从外部 API 拉取数据
后续请求命中内存缓存,响应 < 100ms。
如何调整缓存时间?
市场数据 TTL 已配置化,改 backend/app/core/config.py(或同名环境变量):
CACHE_TTL_HOT_STOCKS: int = 10 # 热门股票排行(交易时段,秒)
CACHE_TTL_FUND_FLOW: int = 15 # 资金流概况
CACHE_TTL_IDLE: int = 3600 # 休市统一 TTL
消费方是 market_data_service/infra.py 的 TTL_TRADING / TTL_IDLE(延迟读取配置的代理,改配置无需改调用点)。分钟 K 线的 TTL 独立在 market_data_service/intraday_cache.py::PERIOD_TTL_MAP。
缩短 TTL 会增加外部 API 调用频率,可能触发东财风控。
内存占用会持续增长吗?
不会。缓存有 TTL 自动过期机制,lru_cache 限制最大条目数。
进程与热重载
后端启动即退出,提示「端口 6789 已有实例在服务」
backend/run.py 起 web 子进程之前会先 bind 探测 6789:已被监听则拒绝启动
(连带不起 worker)。这不是故障——win32 下 uvicorn 用 --host 0.0.0.0 起服务时,
第二个实例 bind 同一端口会连接落给先起的那个,于是出现"改了代码页面没变"的假象。
netstat -ano | findstr :6789 # 查占用 PID
taskkill /PID <pid> /F # 结束旧实例(uvicorn 有父子两个 PID,杀父进程即可)
改了后端代码,页面没变
分两种情况,判据不同:
| 改的是 | 期望行为 | 不符合时的排查 |
|---|---|---|
backend/app/**/*.py(web 侧) | 保存后 1~2s 出现 [supervisor] 检测到变更(…),重启 web 子进程… + 新的 Started server process [PID] | 没有这两行 = 自实现的 mtime 轮询没跑起来(监听范围只有 backend/app,改目录外的文件本就不触发) |
| 调度器 / 任务 / worker 侧代码 | 不热重载,必须重启 run.py | 这是设计代价,见下条 |
web 子进程起不来(如语法错误)时 supervisor 不会自动重起,日志里有 ⚠️ 提示; 把代码改对、保存一次即自动重新拉起。
win32 下 uvicorn reloader 重启子进程走 os.kill(child.pid, signal.CTRL_C_EVENT) 再
child.join(),而该信号在后台 / nohup / 无控制台启动时投不出去 → 子进程既不死也不抛异常,
reloader 永久卡在 join,第一次检测到变更后热重载就彻底停摆,进程树、端口、日志全都正常。
故改为 mtime 轮询 + terminate/kill 自实现。
改了调度器代码,定时任务仍产出旧口径
worker 子进程不随热重载(长耗时融合任务被重载腰斩正是拆进程的原因)。 判据:比对模块 mtime 与 worker 启动时刻——
Get-CimInstance Win32_Process -Filter "name='python.exe'" |
Select-Object ProcessId, CreationDate, CommandLine
模块晚于 worker 启动时间落盘 → 重启 run.py。重启后当日的产出还要手动重跑一次
覆盖旧行(如 quant_calc 是先删目标日行再插入,重跑即覆盖)。
自己起了 python worker.py 却看不到「已取得 scheduler 锁」
多数情况不是故障:GET_LOCK('xuangu_scheduler') 已被别的实例持有(本机常是已在跑的
服务),本进程转入 30s 竞选,该日志在 DEBUG 级,INFO 级看着像"卡住不动"。对方下线后
会自动接管——不要为此加大重试或手删锁。
任务中心一直显示「调度器已停止」/「进行中」
- 「已停止」先看心跳:
runtime.worker.heartbeat_at90s 内为新鲜即表示 worker 活着,host三态local(本进程持有 GET_LOCK)/remote(心跳新鲜)/none(真停了)。XUANGU_ROLE=web的进程里threadAlive恒为 False,用它判断必然误报 - 「进行中」不收敛:残留
running行由sweep_orphan_runs()按两级判活收敛为interrupted(执行进程已退出→立即收敛;判不出死活才回落 2h 时长阈值)。 常态跑 1.5~2h 的全市场链路不会被时长分支误杀
任务与数据
某个定时任务昨天没跑 / 数据是空的,但没有任何告警
三类静默跳过都会这样,逐条核对:
| 现象 | 根因 |
|---|---|
| 凌晨/盘前补跑之后,当日正式时点不跑 | last_success_date 被记成运行日而非应执行日(数据日),last_success >= expected 判成"已完成" |
| 盘前补跑之后当日整点不跑(即使重启也没用) | 调度去重的 last_run_date 记成自然日;启动回填 seed_last_run_date_from_db 同样要按应执行日写 |
| 当日失败后再也不跑 | 启动回填只认有产出的状态(success/partial/skipped),failed 刻意不回填——但当日正式时点若已过则不会再触发,需手动补跑 |
排查入口是任务中心的 app_task_run(按 key × 日期与注册表 SCHEDULED_TASKS 逐槽对账),
步骤级历史看 app_task_step(区分"卡死"与"正常慢")。
页面显示「AI 生成」但我只配了失效的 Key?
不会。凭据 401/403 时产出稿的 source / provenance 有严格三档标注:
local_rule(本地规则口径,琥珀色「本地规则口径 · 非大模型生成」)、
agent_batch(助手离线撰写,中性蓝)、缺省(本次实时调用 → 「AI 生成」)。
调度侧对降级稿记 partial 而非 success,也不记 failed。
部署相关
支持哪些操作系统?
- Windows(主要开发环境)
- macOS / Linux(部署环境)
需要 GPU 吗?
不需要。AI 功能调用远程 LLM API,本地仅运行数据服务。
数据库必须用 MySQL 吗?
当前使用 MySQL 8。理论上 SQLAlchemy 支持其他数据库,但 init.sql 针对 MySQL 编写。
开发相关
如何添加新数据源?
- 在
data_sources/下创建客户端文件,实现数据获取函数 - 在
data_sources/fallback_registry.py的对应数据类型的降级链中登记(含主源/备用源与 tier) - 在
market_data_service/的取数函数(fetchers.py/providers_*.py)中接入 - 单股维度另需在
data_manager/的DOWNLOADER_MAP中登记(/datamgr/download/{code}可下载)
如何确认后端已经起来?
curl http://localhost:6789/api/v1/health # {status, uptime_seconds, db_available, ai_configured, ...}
健康端点在 /api/v1/health(另有 /api/v1/health/ 就绪探针与 /api/v1/health/datasources
细项)。裸 /health 没有任何路由挂载,恒返回 404——脚本里拿 /health 做存活探测会得到
"服务没起"的假结论。
如何运行测试?
cd backend
# 数据接口系统性测试(预期零失败)
python -X utf8 tests/test_data_interfaces.py
# 单文件语法检查
python -m py_compile app/services/market_data_service/fetchers.py
前端与移动端:
npx tsc --noEmit # TypeScript 类型检查
npx vite build # 前端构建
cd mobile && flutter test # 移动端测试
单文件 pytest 必过,多文件同批跑就挂
tests/conftest.py 的 SQLite 库是会话级共享的,用例互不越界必须自己保证。
最常见的四条违反:
| 违反 | 后果 |
|---|---|
用 TestClient.stream() 读永不结束的 SSE 流 | stream() 的 __enter__ 在本环境死锁,挂满超时把整个 pytest 会话拖死。SSE 帧生成器独立成函数,测试直接 await __anext__() |
用例内先 monkeypatch.setattr 再首次导入目标模块 | 该模块导入期把 mock 抄进自己命名空间,teardown 按记录的"原值"恢复的正是这个 mock → 后续整个会话都读到它 |
| 断言全局列表/计数等于精确值 | 共享库意味着别的用例合法地在写,只断言自己的条目 |
| 后台线程写库时测试线程循环读同一张表 | StaticPool 单连接共享,读会话收尾的 rollback 会把未提交的写一起回滚(无异常无告警、值静默消失) |
完整六条(含 async_task=True 的线程时序、导入期 DB 绑定的预热 fixture)见仓库根
AGENTS.md「测试隔离铁律」。排错工具:backend/pyproject.toml 已配
faulthandler_timeout = 300,卡死 5 分钟自动把所有线程栈打到 stderr。