fix(HEL-494): 盘中双免费源故障切换并禁止问天假0覆盖
主源东财失败后自动改走腾讯行情,成功结果写入缓存;两源都失败时返回最近真实快照并标明延迟,不再显示假0。 Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
co-authored by
Cursor
multica-agent
parent
b5d65ecb41
commit
ef13d6feb5
@@ -11,6 +11,7 @@ import time
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from datahub.adapters.base import AdapterError
|
||||
from datahub.adapters.eastmoney import EastmoneyAdapter
|
||||
from datahub.adapters.tencent import TencentAdapter
|
||||
from datahub.codes import resolve_code
|
||||
@@ -71,24 +72,32 @@ def fetch_market_quotes(db: HubDB) -> dict[str, Any]:
|
||||
cached = _read_cache(db, cache_key)
|
||||
if cached is not None:
|
||||
return cached
|
||||
adapter = EastmoneyAdapter()
|
||||
errors: list[str] = []
|
||||
rows: list[dict[str, Any]] = []
|
||||
source = ""
|
||||
try:
|
||||
rows = adapter.fetch_market_quotes()
|
||||
rows = EastmoneyAdapter().fetch_market_quotes()
|
||||
source = "eastmoney:clist"
|
||||
except Exception as exc:
|
||||
raise RealtimeApiError("SOURCE_UNAVAILABLE", f"market quotes unavailable: {exc}") from exc
|
||||
payload = _envelope(
|
||||
rows,
|
||||
{
|
||||
"tier": "provisional",
|
||||
"trade_date": yyyymmdd(now_shanghai()),
|
||||
"source": source,
|
||||
"stale": False,
|
||||
"staleness_seconds": 0,
|
||||
"published_at": isoformat(now_shanghai()),
|
||||
"scope": "market",
|
||||
},
|
||||
)
|
||||
errors.append(f"eastmoney:{exc}")
|
||||
try:
|
||||
listed = _listed_ts_codes(db)
|
||||
if not listed:
|
||||
raise AdapterError("no local stock master for tencent market snapshot")
|
||||
rows = TencentAdapter().fetch_quotes(listed)
|
||||
if len(rows) < 200:
|
||||
raise AdapterError(f"Tencent market snapshot too small: {len(rows)}")
|
||||
source = "tencent:qt"
|
||||
except Exception as backup_exc:
|
||||
errors.append(f"tencent:{backup_exc}")
|
||||
recovered = _load_quotes_lkg(db, cache_key)
|
||||
if recovered is not None:
|
||||
return recovered
|
||||
raise RealtimeApiError(
|
||||
"SOURCE_UNAVAILABLE",
|
||||
"market quotes unavailable: " + ";".join(errors),
|
||||
) from backup_exc
|
||||
payload = _quote_payload(rows, source, scope="market")
|
||||
_write_cache(db, cache_key, payload, QUOTE_TTL, source)
|
||||
return payload
|
||||
|
||||
@@ -110,29 +119,98 @@ def fetch_quotes(db: HubDB, codes: list[str]) -> dict[str, Any]:
|
||||
cached = _read_cache(db, cache_key)
|
||||
if cached is not None:
|
||||
return cached
|
||||
adapter = EastmoneyAdapter()
|
||||
minimum = max(1, int(len(resolved) * 0.5))
|
||||
errors: list[str] = []
|
||||
rows: list[dict[str, Any]] = []
|
||||
source = ""
|
||||
try:
|
||||
rows: list[dict[str, Any]] = []
|
||||
for index in range(0, len(resolved), QUOTE_BATCH):
|
||||
rows.extend(adapter.fetch_quotes(resolved[index:index + QUOTE_BATCH]))
|
||||
rows = _eastmoney_named_quotes(resolved)
|
||||
if len(rows) < minimum:
|
||||
raise AdapterError(f"Eastmoney named quotes too small: {len(rows)}/{len(resolved)}")
|
||||
source = "eastmoney:ulist"
|
||||
except Exception as exc:
|
||||
raise RealtimeApiError("SOURCE_UNAVAILABLE", f"quotes unavailable: {exc}") from exc
|
||||
payload = _envelope(
|
||||
rows,
|
||||
{
|
||||
"tier": "provisional",
|
||||
"trade_date": yyyymmdd(now_shanghai()),
|
||||
"source": source,
|
||||
"stale": False,
|
||||
"staleness_seconds": 0,
|
||||
"published_at": isoformat(now_shanghai()),
|
||||
},
|
||||
)
|
||||
errors.append(f"eastmoney:{exc}")
|
||||
try:
|
||||
rows = TencentAdapter().fetch_quotes(resolved)
|
||||
if len(rows) < minimum:
|
||||
raise AdapterError(f"Tencent named quotes too small: {len(rows)}/{len(resolved)}")
|
||||
source = "tencent:qt"
|
||||
except Exception as backup_exc:
|
||||
errors.append(f"tencent:{backup_exc}")
|
||||
recovered = _load_quotes_lkg(db, cache_key)
|
||||
if recovered is not None:
|
||||
return recovered
|
||||
raise RealtimeApiError(
|
||||
"SOURCE_UNAVAILABLE",
|
||||
"quotes unavailable: " + ";".join(errors),
|
||||
) from backup_exc
|
||||
payload = _quote_payload(rows, source)
|
||||
_write_cache(db, cache_key, payload, QUOTE_TTL, source)
|
||||
return payload
|
||||
|
||||
|
||||
def _eastmoney_named_quotes(codes: list[str]) -> list[dict[str, Any]]:
|
||||
adapter = EastmoneyAdapter()
|
||||
rows: list[dict[str, Any]] = []
|
||||
for index in range(0, len(codes), QUOTE_BATCH):
|
||||
rows.extend(adapter.fetch_quotes(codes[index:index + QUOTE_BATCH]))
|
||||
return rows
|
||||
|
||||
|
||||
def _listed_ts_codes(db: HubDB) -> list[str]:
|
||||
try:
|
||||
rows = db.fetchall(
|
||||
"SELECT ts_code FROM stock_master WHERE list_status = 'L' ORDER BY ts_code"
|
||||
)
|
||||
except Exception:
|
||||
return []
|
||||
return [str(row.get("ts_code") or "") for row in rows if row.get("ts_code")]
|
||||
|
||||
|
||||
def _quote_payload(
|
||||
rows: list[dict[str, Any]],
|
||||
source: str,
|
||||
scope: str = "",
|
||||
) -> dict[str, Any]:
|
||||
meta: dict[str, Any] = {
|
||||
"tier": "provisional",
|
||||
"trade_date": yyyymmdd(now_shanghai()),
|
||||
"source": source,
|
||||
"stale": False,
|
||||
"staleness_seconds": 0,
|
||||
"published_at": isoformat(now_shanghai()),
|
||||
"failover": source.startswith("tencent"),
|
||||
"delay_notice": "",
|
||||
}
|
||||
if scope:
|
||||
meta["scope"] = scope
|
||||
return _envelope(rows, meta)
|
||||
|
||||
|
||||
def _load_quotes_lkg(db: HubDB, cache_key: str) -> dict[str, Any] | None:
|
||||
store = LastKnownGood(db)
|
||||
item = store.load(cache_key)
|
||||
payload = item.get("payload") if item else None
|
||||
if not isinstance(payload, dict):
|
||||
return None
|
||||
data = payload.get("data")
|
||||
if not isinstance(data, list) or not data:
|
||||
return None
|
||||
stamped = dict(payload)
|
||||
meta = dict(stamped.get("meta") or {})
|
||||
stored = str((item or {}).get("stored_at") or "")
|
||||
try:
|
||||
age = max(0, int(time.time() - datetime.fromisoformat(stored).timestamp()))
|
||||
except Exception:
|
||||
age = QUOTE_TTL
|
||||
meta["stale"] = True
|
||||
meta["staleness_seconds"] = age
|
||||
meta["delay_notice"] = f"主备免费行情均暂不可用,显示 {age} 秒前的真实快照"
|
||||
meta["lkg_source"] = str((item or {}).get("source") or meta.get("source") or "")
|
||||
stamped["meta"] = meta
|
||||
return stamped
|
||||
|
||||
|
||||
def fetch_intraday(db: HubDB, code: str, date: str = "") -> dict[str, Any]:
|
||||
ts_code = resolve_code(db, code) or _guess_ts_code(code)
|
||||
if not ts_code:
|
||||
|
||||
Reference in New Issue
Block a user