from __future__ import annotations from datetime import datetime 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.features.market.insights import MarketInsightService from backend.features.market.insights.service import MarketInsightError from backend.features.market.sync import MarketSnapshotService, SnapshotSyncError from backend.http.errors import AppError class MarketService: def __init__( self, gateway: DataGateway, snapshots: MarketSnapshotService, insights: MarketInsightService, ) -> None: self._gateway = gateway self._snapshots = snapshots self._insights = insights 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 entity_detail( self, entity_type: str, identifier: str, requested_date: str | None = None ) -> dict[str, Any]: return self._call( self._snapshots.entity_detail, entity_type, identifier, requested_date ) def refresh_reference(self) -> dict[str, int | str]: return self._call(self._gateway.refresh_reference) def sync_snapshot(self, requested_date: str | None = None) -> dict[str, Any]: return self._call(self._snapshots.sync, requested_date) def sync_realtime( self, requested_date: str | None = None, now: datetime | None = None ) -> dict[str, Any]: return self._call(self._snapshots.sync_realtime, requested_date, now) def workspace(self, key: str, requested_date: str | None = None) -> dict[str, Any]: return self._call(self._snapshots.workspace, key, requested_date) def rotation_members( self, sector_name: str, requested_date: str | None = None ) -> dict[str, Any]: trade_date, representative = self._call( self._snapshots.rotation_member_target, requested_date, sector_name ) return self._call( self._gateway.sector_members, trade_date, sector_name, representative ) def insight( self, key: str, requested_date: str | None, user_id: int ) -> dict[str, Any]: return self._call(self._insights.workspace, key, requested_date, user_id=user_id) def sync_insight( self, key: str, requested_date: str | None, user_id: int ) -> dict[str, Any]: return self._call( self._insights.workspace, key, requested_date, user_id=user_id, force=True ) def theme_detail(self, identifier: str, requested_date: str | None = None) -> dict[str, Any]: return self._call(self._insights.theme_detail, identifier, requested_date) def save_seat_alias( self, seat_name: str, alias_name: str, user_id: int ) -> dict[str, str]: return self._call(self._insights.save_seat_alias, seat_name, alias_name, user_id) @staticmethod def _call(function, *args, **kwargs): try: return function(*args, **kwargs) except ( MarketDataUnavailable, ProviderError, DataQualityError, SnapshotSyncError, MarketInsightError, ) 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, }