rebuild(stage-5): establish market data gateway and charts

This commit is contained in:
leefer
2026-07-30 02:35:42 +08:00
parent 40ad5d6836
commit cf0ab7026f
45 changed files with 2701 additions and 46 deletions
+96
View File
@@ -0,0 +1,96 @@
from __future__ import annotations
from typing import Any
from backend.data.gateway import DataGateway, MarketDataUnavailable
from backend.data.providers.base import ProviderError
from backend.data.quality import DataQualityError
from backend.http.errors import AppError
class MarketService:
def __init__(self, gateway: DataGateway) -> None:
self._gateway = gateway
def context(self, requested_date: str | None = None) -> dict[str, Any]:
context = self._call(self._gateway.trade_context, requested_date)
return _context(context)
def summary(self, requested_date: str | None = None) -> dict[str, Any]:
result = self._call(self._gateway.summary, requested_date)
return {"context": _context(result["context"]), "values": result["values"]}
def search(self, query: str) -> dict[str, Any]:
normalized = " ".join(query.split())
items = self._gateway.search(normalized) if normalized else ()
labels = {"stock": "股票", "sector": "板块", "theme": "题材", "index": "指数"}
groups = []
for entity_type in ("stock", "sector", "theme", "index"):
groups.append(
{
"entity_type": entity_type,
"label": labels[entity_type],
"items": [
{
"entity_type": item.entity_type,
"identifier": item.identifier,
"code": item.code,
"name": item.name,
"sector": item.sector,
}
for item in items
if item.entity_type == entity_type
],
}
)
return {"query": normalized, "groups": groups}
def chart(self, entity_type: str, identifier: str, interval: str) -> dict[str, Any]:
series = self._call(self._gateway.chart, entity_type, identifier, interval)
return {
"entity_type": series.entity.entity_type,
"identifier": series.entity.identifier,
"code": series.entity.code,
"name": series.entity.name,
"interval": series.interval,
"trade_date": series.trade_date,
"observed_at": series.metadata.observed_at,
"previous_close": series.previous_close,
"range_start": "09:30" if interval == "minute" else None,
"range_end": "15:00" if interval == "minute" else None,
"points": [
{
"time": point.time,
"open": point.open,
"high": point.high,
"low": point.low,
"close": point.close,
"volume": point.volume,
"amount": point.amount,
"average": point.average,
}
for point in series.points
],
}
def refresh_reference(self) -> dict[str, int | str]:
return self._call(self._gateway.refresh_reference)
@staticmethod
def _call(function, *args):
try:
return function(*args)
except (MarketDataUnavailable, ProviderError, DataQualityError) as exc:
raise AppError("market_data_unavailable", str(exc), 503) from exc
def _context(context) -> dict[str, Any]:
return {
"requested_date": context.requested_date,
"actual_date": context.actual_date,
"previous_date": context.previous_date,
"observed_at": context.observed_at,
"state": context.state.value if context.state else None,
"carried_forward": context.carried_forward,
"message": context.message,
}