布局:表格长错误/备注统一单行大白话摘要+截断(.ellip),完整原文放 title 悬停;当前来源列 nowrap 防逐字竖排;压力态(长错误/多接口/多观测行) 1920×920、1920×1080、1024 均无横向溢出、血缘 1080P 一屏。 血缘:按真实调用代码修正——ifind_wencai 的真实消费者是股票池事件补充 (pools/service.py:82),不是问师;新增 ifind_history 条目对应问师·趋势/ 宏观思维模型的可选指数/ETF 矩阵 (mentor/service.py:433 ifind.history, 未配置时 fail-open);新增 3 项自动测试锁定映射并以源码调用点为证。 动效:延迟/成功率仅在真实采样值变化时翻动高亮(data-rate flash); spark-end/血缘心跳按行/节点错峰相位;页面隐藏时动态循环不做任何工作; LINK 呼吸(gnode-pulse)与真实调用脉冲(node-ping)语义区分保持。 测试:全套 194 项通过。 Co-authored-by: multica-agent <github@multica.ai>
298 lines
12 KiB
Python
298 lines
12 KiB
Python
"""Read-only lineage/impact inventory (HEL-543).
|
|
|
|
Answers, without changing any routing decision: for a given main-site data
|
|
item, which datahub dataset backs it, which provider/interface currently
|
|
serves it (primary and backup), and — when a provider/interface is
|
|
unhealthy — which datasets and, best-effort, which main-site consumers are
|
|
affected.
|
|
|
|
Every row cites where it was verified so a reviewer does not have to trust
|
|
a paraphrase:
|
|
|
|
- ``v1_endpoint``/``primary_source``/``backup_source`` are taken verbatim
|
|
from ``datahub/serving.py`` (the ``source=`` string literal passed to
|
|
``_published_rows``/``_official_meta``) or from the provider/interface
|
|
pairs wired into ``datahub/realtime_serve.py`` for HEL-543.
|
|
- ``known_consumers`` lists only call sites this round actually found via
|
|
code search in the ``xiaobai-review`` website tree (cited as
|
|
``file:line`` in the comment above each dataset). Anything not backed by
|
|
a citation is left out rather than guessed; a fuller page-by-page map is
|
|
tracked separately (HEL-549) and can extend this table later without
|
|
touching its shape.
|
|
|
|
This module never talks to a provider and never mutates anything; it only
|
|
reads ``provider_health``/``provider_call_log`` (HEL-543) and the existing
|
|
``publications``/``batches`` tables to attach live status to each row.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
# Verified against backend/features/screener/data_sync.py (calendar,
|
|
# stock_basic, daily, daily_basic, index_daily-as-benchmark, stk_auction,
|
|
# moneyflow, ths_hot, dc_hot all called via `self.client.query(...)`) and
|
|
# backend/features/heaven/market_context.py (stock_basic, index_daily via
|
|
# `self._tushare_client().query(...)` / `client.query(...)`).
|
|
DATASETS: list[dict[str, Any]] = [
|
|
{
|
|
"dataset": "calendar",
|
|
"tier": "official",
|
|
"update_freq": "每日 08:45 预检",
|
|
"v1_endpoint": "/v1/calendar",
|
|
"primary_source": "tushare:trade_cal",
|
|
"backup_source": None,
|
|
"known_consumers": ["智能选股(data_sync.py 交易日历解析)", "问天(交易日推算)"],
|
|
},
|
|
{
|
|
"dataset": "stocks",
|
|
"tier": "official",
|
|
"update_freq": "每日 20:00 / 23:10",
|
|
"v1_endpoint": "/v1/stocks",
|
|
"primary_source": "tushare:stock_basic",
|
|
"backup_source": None,
|
|
"known_consumers": ["智能选股(股票主档)", "问天(market_context.py 股票代码/名称解析)"],
|
|
},
|
|
{
|
|
"dataset": "daily",
|
|
"tier": "official",
|
|
"update_freq": "盘后 15:05 · 重试至 23:30",
|
|
"v1_endpoint": "/v1/bars/daily",
|
|
"primary_source": "tushare:daily",
|
|
"backup_source": None,
|
|
"known_consumers": ["智能选股(data_sync.py 日K因子)", "交易复盘/个股详情日K图表"],
|
|
},
|
|
{
|
|
"dataset": "valuation",
|
|
"tier": "official",
|
|
"update_freq": "盘后 15:05 · 复核 20:00",
|
|
"v1_endpoint": "/v1/valuation",
|
|
"primary_source": "tushare:daily_basic",
|
|
"backup_source": None,
|
|
"known_consumers": ["智能选股(data_sync.py 估值因子)"],
|
|
},
|
|
{
|
|
"dataset": "moneyflow",
|
|
"tier": "official",
|
|
"update_freq": "盘后 15:05 · 重试至 23:30",
|
|
"v1_endpoint": "/v1/moneyflow",
|
|
"primary_source": "tushare:moneyflow",
|
|
"backup_source": None,
|
|
"known_consumers": ["智能选股(data_sync.py 资金流因子)", "个股详情资金流"],
|
|
},
|
|
{
|
|
"dataset": "auction",
|
|
"tier": "official",
|
|
"update_freq": "盘后 15:05",
|
|
"v1_endpoint": "/v1/auction",
|
|
"primary_source": "tushare:stk_auction",
|
|
"backup_source": None,
|
|
"known_consumers": ["智能选股(data_sync.py 竞价快照)", "竞价板块"],
|
|
},
|
|
{
|
|
"dataset": "index_daily",
|
|
"tier": "official",
|
|
"update_freq": "盘后 15:10 · 重试至 23:30",
|
|
"v1_endpoint": "/v1/indexes/bars",
|
|
"primary_source": "tushare:index_daily",
|
|
"backup_source": None,
|
|
"known_consumers": ["问天(market_context.py 指数近20日走势)", "智能选股(基准回看)"],
|
|
},
|
|
{
|
|
"dataset": "limit_events",
|
|
"tier": "official",
|
|
"update_freq": "盘后 16:40 · 重试至 23:30",
|
|
"v1_endpoint": "/v1/limit-events",
|
|
"primary_source": "tushare:limit_list_d",
|
|
"backup_source": None,
|
|
"known_consumers": ["涨停梯队(历史/盘后视图)"],
|
|
},
|
|
{
|
|
"dataset": "popularity",
|
|
"tier": "official",
|
|
"update_freq": "盘后 22:40",
|
|
"v1_endpoint": "/v1/popularity",
|
|
"primary_source": "tushare:ths_hot+dc_hot",
|
|
"backup_source": None,
|
|
"known_consumers": ["人气榜", "智能选股(data_sync.py 人气因子)"],
|
|
},
|
|
{
|
|
"dataset": "dragon_tiger",
|
|
"tier": "official",
|
|
"update_freq": "盘后 16:45 · 重试至 23:30",
|
|
"v1_endpoint": "/v1/dragon-tiger",
|
|
"primary_source": "tushare:hm_detail",
|
|
"backup_source": None,
|
|
"known_consumers": ["龙虎榜"],
|
|
},
|
|
{
|
|
"dataset": "sector_daily",
|
|
"tier": "official",
|
|
"update_freq": "盘后 15:20 · 重试至 23:30",
|
|
"v1_endpoint": "/v1/sectors",
|
|
"primary_source": "tushare:ths_daily+dc_index+sw_daily",
|
|
"backup_source": None,
|
|
"known_consumers": ["主题轮动", "板块梯队"],
|
|
},
|
|
{
|
|
"dataset": "quotes_latest",
|
|
"tier": "provisional",
|
|
"update_freq": "盘中 · 缓存 60s",
|
|
"v1_endpoint": "/v1/quotes/latest",
|
|
"primary_source": "eastmoney:ulist/clist",
|
|
"backup_source": "tencent:qt",
|
|
"known_consumers": ["竞价/股票池盘中价格", "情绪周期盘中快照"],
|
|
},
|
|
{
|
|
"dataset": "index_quotes",
|
|
"tier": "provisional",
|
|
"update_freq": "盘中 · 缓存 60s",
|
|
"v1_endpoint": "/v1/indexes/quotes",
|
|
"primary_source": "eastmoney:ulist",
|
|
"backup_source": "tencent:qt",
|
|
"known_consumers": ["首页大盘指数条"],
|
|
},
|
|
{
|
|
"dataset": "sectors_quote",
|
|
"tier": "provisional",
|
|
"update_freq": "盘中 · 缓存 60s",
|
|
"v1_endpoint": "/v1/sectors/quote",
|
|
"primary_source": "eastmoney:sw",
|
|
"backup_source": None,
|
|
"known_consumers": ["主题轮动盘中板块报价"],
|
|
},
|
|
{
|
|
"dataset": "limit_pool",
|
|
"tier": "provisional",
|
|
"update_freq": "盘中 · 缓存 60s",
|
|
"v1_endpoint": "/v1/limit-pool",
|
|
"primary_source": "eastmoney:zt_pool",
|
|
"backup_source": None,
|
|
"known_consumers": ["涨停梯队盘中视图"],
|
|
},
|
|
{
|
|
"dataset": "intraday_points",
|
|
"tier": "provisional",
|
|
"update_freq": "盘中 · 缓存 20s",
|
|
"v1_endpoint": "/v1/intraday/points",
|
|
"primary_source": "eastmoney:trends2",
|
|
"backup_source": None,
|
|
"known_consumers": ["个股详情分时图"],
|
|
},
|
|
{
|
|
"dataset": "ifind_wencai",
|
|
"tier": "licensed",
|
|
"update_freq": "按需调用",
|
|
"v1_endpoint": "/v1/query (api_name=ifind_wencai)",
|
|
"primary_source": "ifind:wencai",
|
|
"backup_source": None,
|
|
# Real call site: backend/features/pools/service.py:82 (wencai(query,"stock"))
|
|
# feeds 股票池's ifind_event_enrichment_v1 (涨停/炸板/跌停原因、首末
|
|
# 涨停时间、开板次数). 问师 does NOT call wencai anywhere — its main
|
|
# body is LLM + 主站市场快照 (mentor/service.py builds context from
|
|
# dashboard/popularity snapshots only).
|
|
"known_consumers": ["股票池(涨停/炸板/跌停事件补充 enrichment,需 iFinD 凭证)"],
|
|
},
|
|
{
|
|
"dataset": "ifind_history",
|
|
"tier": "licensed",
|
|
"update_freq": "问师问询时按需 · 45 日回看",
|
|
"v1_endpoint": "/v1/query (api_name=ifind_history)",
|
|
"primary_source": "ifind:history",
|
|
"backup_source": None,
|
|
# Real call site: backend/features/mentor/service.py:433-457
|
|
# (_mentor_market_matrix → ifind.history(close/volume/amount, 45 日回看)).
|
|
# Only the trend/macro thinking-model profiles use it, and only when
|
|
# iFinD is configured — it fails open to [] otherwise. 问师其余子能力
|
|
# (本体问答/低吸/人气上下文等) 不依赖 iFinD.
|
|
"known_consumers": [
|
|
"问师·趋势思维模型(指数动量矩阵,可选)",
|
|
"问师·宏观思维模型(宽基指数与核心ETF矩阵,可选)",
|
|
],
|
|
},
|
|
]
|
|
|
|
_KNOWN_PROVIDERS_BY_SOURCE_PREFIX = ("tushare", "eastmoney", "tencent", "ifind")
|
|
|
|
|
|
def _providers_for(primary_source: str, backup_source: str | None) -> list[str]:
|
|
providers: list[str] = []
|
|
for source in (primary_source, backup_source or ""):
|
|
for provider in _KNOWN_PROVIDERS_BY_SOURCE_PREFIX:
|
|
if source.startswith(provider) and provider not in providers:
|
|
providers.append(provider)
|
|
return providers
|
|
|
|
|
|
def snapshot(db: Any, trade_date: str = "") -> list[dict[str, Any]]:
|
|
"""Attach live status to the static lineage table. Read-only; never
|
|
raises (a per-row status lookup failure just leaves that row's status
|
|
empty rather than failing the whole snapshot)."""
|
|
result: list[dict[str, Any]] = []
|
|
for entry in DATASETS:
|
|
row = dict(entry)
|
|
providers = _providers_for(entry["primary_source"], entry.get("backup_source"))
|
|
row["providers"] = providers
|
|
live: list[dict[str, Any]] = []
|
|
try:
|
|
if db is not None and providers:
|
|
placeholders = ",".join("?" for _ in providers)
|
|
live = db.fetchall(
|
|
f"SELECT provider, interface, state, last_error, last_fallback_reason, "
|
|
f"consec_failures, updated_at FROM provider_health WHERE provider IN ({placeholders})",
|
|
tuple(providers),
|
|
)
|
|
except Exception:
|
|
live = []
|
|
row["live_provider_health"] = live
|
|
if entry["tier"] == "official":
|
|
pub = None
|
|
try:
|
|
if db is not None and trade_date:
|
|
pub = db.fetchone(
|
|
"SELECT dataset, trade_date, state, published_at FROM publications "
|
|
"WHERE dataset = ? AND trade_date = ?",
|
|
(entry["dataset"], trade_date),
|
|
)
|
|
except Exception:
|
|
pub = None
|
|
row["publication"] = pub
|
|
result.append(row)
|
|
return result
|
|
|
|
|
|
def affected(db: Any, provider: str = "", interface: str = "") -> list[dict[str, Any]]:
|
|
"""Read-only: which datasets/pages are impacted by a given provider (and,
|
|
optionally, a specific interface) right now. Does not change routing."""
|
|
provider = str(provider or "").strip()
|
|
interface = str(interface or "").strip()
|
|
result: list[dict[str, Any]] = []
|
|
for entry in DATASETS:
|
|
providers = _providers_for(entry["primary_source"], entry.get("backup_source"))
|
|
if provider and provider not in providers:
|
|
continue
|
|
row = dict(entry)
|
|
row["providers"] = providers
|
|
health: list[dict[str, Any]] = []
|
|
try:
|
|
if db is not None:
|
|
if interface:
|
|
health = db.fetchall(
|
|
"SELECT provider, interface, state, last_error, last_fallback_reason, "
|
|
"consec_failures, updated_at FROM provider_health "
|
|
"WHERE provider = ? AND interface = ?",
|
|
(provider, interface),
|
|
)
|
|
elif providers:
|
|
placeholders = ",".join("?" for _ in providers)
|
|
health = db.fetchall(
|
|
f"SELECT provider, interface, state, last_error, last_fallback_reason, "
|
|
f"consec_failures, updated_at FROM provider_health WHERE provider IN ({placeholders})",
|
|
tuple(providers),
|
|
)
|
|
except Exception:
|
|
health = []
|
|
row["live_provider_health"] = health
|
|
result.append(row)
|
|
return result
|