Files
xiaobaifupan/next/backend/data/providers/eastmoney.py
T

128 lines
4.7 KiB
Python

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")
@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("该标的暂无展示分时数据")