rebuild(stage-5): establish market data gateway and charts
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
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)
|
||||
|
||||
@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("该标的暂无展示分时数据")
|
||||
Reference in New Issue
Block a user