from __future__ import annotations import json import urllib.error import urllib.parse import urllib.request from datetime import datetime from zoneinfo import ZoneInfo from backend.data.contracts import ( DataSource, DataUsage, ObservationMetadata, ProviderResult, SnapshotState, ) from backend.data.providers.base import ProviderError SHANGHAI = ZoneInfo("Asia/Shanghai") INDEX_CODES = {"000001.SH": "1.000001", "399001.SZ": "0.399001", "399006.SZ": "0.399006"} class EastmoneyProvider: source = DataSource.EASTMONEY url = "https://push2delay.eastmoney.com/api/qt/stock/trends2/get" def __init__(self, timeout: int = 6) -> None: self._timeout = timeout @property def configured(self) -> bool: return True def calendar(self, start_date: str, end_date: str) -> ProviderResult: raise ProviderError("The display provider is not a calendar authority") def entities(self) -> ProviderResult: raise ProviderError("The display provider is not an entity authority") def daily(self, entity_type: str, identifier: str, end_date: str) -> ProviderResult: raise ProviderError("The display provider does not supply canonical daily bars") def minute(self, entity_type: str, identifier: str, trade_date: str) -> ProviderResult: secid = self._secid(entity_type, identifier) params = urllib.parse.urlencode( { "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", } ) request = urllib.request.Request( f"{self.url}?{params}", headers={"Accept": "application/json", "User-Agent": "XiaobaiReview/2"}, ) try: with urllib.request.urlopen(request, timeout=self._timeout) as response: payload = json.loads(response.read().decode("utf-8")) except (urllib.error.URLError, TimeoutError, OSError, json.JSONDecodeError) as exc: raise ProviderError("展示行情请求失败") from exc data = payload.get("data") or {} rows = [] for raw in data.get("trends") or []: fields = str(raw).split(",") if len(fields) < 8 or " " not in fields[0]: continue date, time = fields[0].split(" ", 1) if date != trade_date or not "09:30" <= time[:5] <= "15:00": continue rows.append( { "time": fields[0], "open": fields[1], "close": fields[2], "high": fields[3], "low": fields[4], "volume": fields[5], "amount": fields[6], "avgPrice": fields[7], "preClose": data.get("preClose"), } ) metadata = ObservationMetadata( source=self.source, observed_at=datetime.now(SHANGHAI), unit="yuan/share", adjustment="unadjusted", freshness_seconds=0, coverage=1 if rows else 0, state=SnapshotState.REALTIME, usage=DataUsage.DISPLAY, ) return ProviderResult(tuple(rows), metadata) def snapshot_inputs( self, trade_date: str, previous_trade_date: str ) -> dict[str, ProviderResult | dict[str, object]]: raise ProviderError("The display provider cannot build market snapshots") def sector_members(self, representative: str, trade_date: str) -> ProviderResult: raise ProviderError("The display provider is not the constituent authority") def market_insight( self, kind: str, trade_date: str, previous_trade_date: str = "", identifier: str = "", ) -> dict[str, ProviderResult | None]: raise ProviderError("The display provider cannot supply market insight archives") def realtime_snapshots( self, identifiers: tuple[str, ...], start_time: str, end_time: str ) -> ProviderResult: raise ProviderError("The display provider cannot supply calculation snapshots") def screener_inputs(self, trade_dates: tuple[str, ...]) -> dict[str, ProviderResult | None]: raise ProviderError("The display provider cannot supply screener factors") @staticmethod def _secid(entity_type: str, identifier: str) -> str: if entity_type == "index" and identifier in INDEX_CODES: return INDEX_CODES[identifier] code = identifier.split(".")[0] if entity_type == "stock" and len(code) == 6 and code.isdigit(): market = "1" if code.startswith(("5", "6", "9")) else "0" return f"{market}.{code}" raise ProviderError("该标的暂无展示分时数据")