128 lines
4.7 KiB
Python
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("该标的暂无展示分时数据")
|