85 lines
3.1 KiB
Python
85 lines
3.1 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from datetime import datetime, timedelta, timezone
|
|
from typing import TYPE_CHECKING, Any, Callable
|
|
|
|
from backend.data.providers.ifind_client import IfindHttpClient
|
|
from backend.data.providers.tushare_client import TushareClient, TushareError
|
|
|
|
if TYPE_CHECKING:
|
|
from database import ReviewDatabase
|
|
|
|
|
|
CHINA_TIMEZONE = timezone(timedelta(hours=8))
|
|
|
|
def _display_date(value: str) -> str:
|
|
text = str(value or "").replace("-", "")
|
|
if len(text) != 8:
|
|
return str(value or "")
|
|
return f"{text[:4]}-{text[4:6]}-{text[6:]}"
|
|
|
|
|
|
class MarketInsightsContextMixin:
|
|
def __init__(
|
|
self,
|
|
database: ReviewDatabase,
|
|
client: TushareClient,
|
|
now_provider: Callable[[], datetime] | None = None,
|
|
ifind: IfindHttpClient | None = None,
|
|
) -> None:
|
|
self.database = database
|
|
self.client = client
|
|
self._now_provider = now_provider or (lambda: datetime.now(CHINA_TIMEZONE))
|
|
self.ifind = ifind
|
|
|
|
def _trade_context(self, requested_date: str) -> tuple[str, str]:
|
|
"""Resolve trading dates without making cached feature pages depend on Tushare uptime."""
|
|
requested = str(requested_date or "").replace("-", "")
|
|
try:
|
|
return self.client.resolve_trade_context(requested)
|
|
except TushareError:
|
|
latest = self.database.get_latest_real_snapshot(requested) or {}
|
|
trade_date = str(
|
|
(latest.get("meta") or {}).get("trade_date")
|
|
or latest.get("_snapshot_date")
|
|
or requested
|
|
).replace("-", "")
|
|
previous = self.database.get_latest_real_snapshot(trade_date, strictly_before=True) or {}
|
|
previous_date = str(
|
|
(previous.get("meta") or {}).get("trade_date")
|
|
or previous.get("_snapshot_date")
|
|
or ""
|
|
).replace("-", "")
|
|
return trade_date, previous_date
|
|
|
|
def _latest_feature_snapshot(self, kind: str, trade_date: str) -> dict[str, Any] | None:
|
|
return self.database.get_latest_data_snapshot(kind, "", trade_date)
|
|
|
|
def _stock_master(self) -> dict[str, dict[str, Any]]:
|
|
rows = self.database.list_stock_master()
|
|
if not rows:
|
|
rows = self.client.query(
|
|
"stock_basic",
|
|
{"list_status": "L"},
|
|
"ts_code,name,industry,market,list_date",
|
|
)
|
|
self.database.upsert_stock_master(rows)
|
|
rows = self.database.list_stock_master()
|
|
return {str(row.get("ts_code") or ""): row for row in rows}
|
|
|
|
@staticmethod
|
|
def _parse_concepts(value: Any) -> list[str]:
|
|
if isinstance(value, list):
|
|
return [str(item) for item in value if str(item).strip()]
|
|
text = str(value or "").strip()
|
|
if not text:
|
|
return []
|
|
try:
|
|
parsed = json.loads(text)
|
|
if isinstance(parsed, list):
|
|
return [str(item) for item in parsed if str(item).strip()]
|
|
except json.JSONDecodeError:
|
|
pass
|
|
return [part.strip() for part in text.split(",") if part.strip()]
|