fix(HEL-482): 开盘前分时回退最近交易日,并接通中枢失败回旧通道
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
605f97e5df
commit
3d2c1252f1
@@ -14,6 +14,7 @@ from datahub.numbers import finite_number, round4
|
||||
EASTMONEY_INDEX_URL = "https://push2.eastmoney.com/api/qt/ulist.np/get"
|
||||
EASTMONEY_CLIST_URL = "https://push2.eastmoney.com/api/qt/clist/get"
|
||||
TRENDS_URL = "https://push2delay.eastmoney.com/api/qt/stock/trends2/get"
|
||||
HIS_TRENDS_URL = "https://push2his.eastmoney.com/api/qt/stock/trends2/get"
|
||||
BROWSER_UA = (
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36"
|
||||
@@ -166,7 +167,7 @@ class EastmoneyAdapter(MarketAdapter):
|
||||
)
|
||||
return result
|
||||
|
||||
def fetch_intraday(self, ts_code: str) -> dict[str, Any]:
|
||||
def fetch_intraday(self, ts_code: str, date: str = "") -> dict[str, Any]:
|
||||
code = str(ts_code or "").upper()
|
||||
if code in INDEX_SECIDS:
|
||||
secid = INDEX_SECIDS[code]
|
||||
@@ -178,25 +179,32 @@ class EastmoneyAdapter(MarketAdapter):
|
||||
secid = f"{market}.{symbol}"
|
||||
entity = "stock"
|
||||
identifier = symbol
|
||||
payload = self._get_json(
|
||||
TRENDS_URL,
|
||||
{
|
||||
"secid": secid,
|
||||
"fields1": "f1,f2,f3,f4,f5,f6,f7,f8,f9,f10,f11,f12,f13",
|
||||
"fields2": "f51,f52,f53,f54,f55,f56,f57,f58",
|
||||
"iscr": "0",
|
||||
"ndays": "1",
|
||||
},
|
||||
referer="https://quote.eastmoney.com/",
|
||||
)
|
||||
data = payload.get("data") or {}
|
||||
points = []
|
||||
for raw in data.get("trends") or []:
|
||||
point = _parse_trend(raw)
|
||||
if point:
|
||||
points.append(point)
|
||||
params = {
|
||||
"secid": secid,
|
||||
"fields1": "f1,f2,f3,f4,f5,f6,f7,f8,f9,f10,f11,f12,f13",
|
||||
"fields2": "f51,f52,f53,f54,f55,f56,f57,f58",
|
||||
"iscr": "0",
|
||||
}
|
||||
data: dict[str, Any] = {}
|
||||
points: list[dict[str, Any]] = []
|
||||
last_error: Exception | None = None
|
||||
for url, ndays in ((TRENDS_URL, "1"), (TRENDS_URL, "5"), (HIS_TRENDS_URL, "5")):
|
||||
try:
|
||||
payload = self._get_json(
|
||||
url,
|
||||
{**params, "ndays": ndays},
|
||||
referer="https://quote.eastmoney.com/",
|
||||
)
|
||||
except AdapterError as exc:
|
||||
last_error = exc
|
||||
continue
|
||||
data = payload.get("data") or {}
|
||||
parsed = [point for raw in data.get("trends") or [] if (point := _parse_trend(raw))]
|
||||
points = _preferred_session(parsed, date)
|
||||
if points:
|
||||
break
|
||||
if not points:
|
||||
raise AdapterError("No intraday chart data returned")
|
||||
raise AdapterError("No intraday chart data returned") from last_error
|
||||
return {
|
||||
"entity_type": entity,
|
||||
"identifier": identifier,
|
||||
@@ -227,6 +235,23 @@ class EastmoneyAdapter(MarketAdapter):
|
||||
raise AdapterError(f"eastmoney request failed: {exc}") from exc
|
||||
|
||||
|
||||
def _preferred_session(points: list[dict[str, Any]], preferred_date: str = "") -> list[dict[str, Any]]:
|
||||
if not points:
|
||||
return []
|
||||
want = ""
|
||||
digits = str(preferred_date or "").replace("-", "")[:8]
|
||||
if len(digits) == 8 and digits.isdigit():
|
||||
want = f"{digits[:4]}-{digits[4:6]}-{digits[6:8]}"
|
||||
if want:
|
||||
matched = [point for point in points if str(point.get("date") or "") == want]
|
||||
if matched:
|
||||
return matched
|
||||
latest = max(str(point.get("date") or "") for point in points)
|
||||
if not latest:
|
||||
return points
|
||||
return [point for point in points if str(point.get("date") or "") == latest]
|
||||
|
||||
|
||||
def _parse_trend(raw: Any) -> dict[str, Any] | None:
|
||||
text = str(raw or "")
|
||||
parts = text.split(",")
|
||||
@@ -237,11 +262,14 @@ def _parse_trend(raw: Any) -> dict[str, Any] | None:
|
||||
when = datetime.strptime(stamp, "%Y-%m-%d %H:%M")
|
||||
except ValueError:
|
||||
return None
|
||||
close = round4(finite_number(parts[2]))
|
||||
if close <= 0:
|
||||
return None
|
||||
return {
|
||||
"time": when.strftime("%H:%M"),
|
||||
"date": when.strftime("%Y-%m-%d"),
|
||||
"open": round4(finite_number(parts[1])),
|
||||
"close": round4(finite_number(parts[2])),
|
||||
"close": close,
|
||||
"high": round4(finite_number(parts[3])),
|
||||
"low": round4(finite_number(parts[4])),
|
||||
"avg_price": round4(finite_number(parts[7] if len(parts) > 7 else parts[2])),
|
||||
|
||||
@@ -14,6 +14,7 @@ from datahub.adapters.eastmoney import EastmoneyAdapter
|
||||
from datahub.adapters.tencent import TencentAdapter
|
||||
from datahub.codes import resolve_code
|
||||
from datahub.db import HubDB
|
||||
from datahub.governance.lkg import LastKnownGood
|
||||
from datahub.timeutil import isoformat, now_shanghai, yyyymmdd
|
||||
|
||||
QUOTE_TTL = 60
|
||||
@@ -108,10 +109,13 @@ def fetch_intraday(db: HubDB, code: str, date: str = "") -> dict[str, Any]:
|
||||
return cached
|
||||
adapter = EastmoneyAdapter()
|
||||
try:
|
||||
payload_data = adapter.fetch_intraday(ts_code)
|
||||
payload_data = adapter.fetch_intraday(ts_code, date)
|
||||
source = "eastmoney:trends2"
|
||||
except Exception as exc:
|
||||
raise RealtimeApiError("SOURCE_UNAVAILABLE", f"intraday unavailable: {exc}") from exc
|
||||
recovered = _load_intraday_lkg(db, ts_code, date)
|
||||
if recovered is None:
|
||||
raise RealtimeApiError("SOURCE_UNAVAILABLE", f"intraday unavailable: {exc}") from exc
|
||||
return recovered
|
||||
payload = _envelope(
|
||||
payload_data,
|
||||
{
|
||||
@@ -127,6 +131,47 @@ def fetch_intraday(db: HubDB, code: str, date: str = "") -> dict[str, Any]:
|
||||
return payload
|
||||
|
||||
|
||||
def _load_intraday_lkg(db: HubDB, ts_code: str, date: str = "") -> dict[str, Any] | None:
|
||||
store = LastKnownGood(db)
|
||||
keys = [f"intraday:{ts_code}:{date or 'today'}"]
|
||||
if date:
|
||||
keys.append(f"intraday:{ts_code}:today")
|
||||
for key in keys:
|
||||
item = store.load(key)
|
||||
payload = _lkg_payload(item)
|
||||
if payload is not None:
|
||||
return payload
|
||||
row = db.fetchone(
|
||||
"SELECT * FROM last_known_good WHERE cache_key LIKE ? ORDER BY stored_at DESC LIMIT 1",
|
||||
(f"intraday:{ts_code}:%",),
|
||||
)
|
||||
if not row:
|
||||
return None
|
||||
try:
|
||||
raw = json.loads(row["payload"])
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
return _mark_stale(raw) if isinstance(raw, dict) else None
|
||||
|
||||
|
||||
def _lkg_payload(item: dict[str, Any] | None) -> dict[str, Any] | None:
|
||||
if not item:
|
||||
return None
|
||||
payload = item.get("payload")
|
||||
return _mark_stale(payload) if isinstance(payload, dict) else None
|
||||
|
||||
|
||||
def _mark_stale(payload: dict[str, Any]) -> dict[str, Any] | None:
|
||||
data = payload.get("data")
|
||||
if not isinstance(data, dict) or not data.get("points"):
|
||||
return None
|
||||
stamped = dict(payload)
|
||||
meta = dict(stamped.get("meta") or {})
|
||||
meta["stale"] = True
|
||||
stamped["meta"] = meta
|
||||
return stamped
|
||||
|
||||
|
||||
def _guess_ts_code(code: str) -> str | None:
|
||||
raw = str(code or "").strip().upper()
|
||||
if "." in raw:
|
||||
|
||||
Reference in New Issue
Block a user