Fundamental_Analysis/backend/app/data_providers/tushare.py
xucheng ff7dc0c95a feat(backend): introduce DataManager and multi-provider; analysis orchestration; streaming endpoints; remove legacy tushare_client; enhance logging
feat(frontend): integrate Prisma and reports API/pages

chore(config): add data_sources.yaml; update analysis-config.json

docs: add 2025-11-03 dev log; update user guide

scripts: enhance dev.sh; add tushare_legacy_client

deps: update backend and frontend dependencies
2025-11-03 21:48:08 +08:00

133 lines
5.1 KiB
Python

from .base import BaseDataProvider
from typing import Any, Dict, List, Optional
import httpx
import logging
import asyncio
logger = logging.getLogger(__name__)
TUSHARE_PRO_URL = "https://api.tushare.pro"
class TushareProvider(BaseDataProvider):
def _initialize(self):
if not self.token:
raise ValueError("Tushare API token not provided.")
# Use httpx.AsyncClient directly
self._client = httpx.AsyncClient(timeout=30)
async def _query(
self,
api_name: str,
params: Optional[Dict[str, Any]] = None,
fields: Optional[str] = None,
) -> List[Dict[str, Any]]:
payload = {
"api_name": api_name,
"token": self.token,
"params": params or {},
}
if "limit" not in payload["params"]:
payload["params"]["limit"] = 5000
if fields:
payload["fields"] = fields
logger.info(f"Querying Tushare API '{api_name}' with params: {params}")
try:
resp = await self._client.post(TUSHARE_PRO_URL, json=payload)
resp.raise_for_status()
data = resp.json()
if data.get("code") != 0:
err_msg = data.get("msg") or "Unknown Tushare error"
logger.error(f"Tushare API error for '{api_name}': {err_msg}")
raise RuntimeError(f"{api_name}: {err_msg}")
fields_def = data.get("data", {}).get("fields", [])
items = data.get("data", {}).get("items", [])
rows: List[Dict[str, Any]] = []
for it in items:
row = {fields_def[i]: it[i] for i in range(len(fields_def))}
rows.append(row)
logger.info(f"Tushare API '{api_name}' returned {len(rows)} rows.")
return rows
except httpx.HTTPStatusError as e:
logger.error(f"HTTP error calling Tushare API '{api_name}': {e.response.status_code} - {e.response.text}")
raise
except Exception as e:
logger.error(f"Exception calling Tushare API '{api_name}': {e}")
raise
async def get_stock_basic(self, stock_code: str) -> Optional[Dict[str, Any]]:
try:
rows = await self._query(
api_name="stock_basic",
params={"ts_code": stock_code},
)
return rows[0] if rows else None
except Exception as e:
logger.error(f"Tushare get_stock_basic failed for {stock_code}: {e}")
return None
async def get_daily_price(self, stock_code: str, start_date: str, end_date: str) -> List[Dict[str, Any]]:
try:
return await self._query(
api_name="daily",
params={
"ts_code": stock_code,
"start_date": start_date,
"end_date": end_date,
},
)
except Exception as e:
logger.error(f"Tushare get_daily_price failed for {stock_code}: {e}")
return []
async def get_financial_statements(self, stock_code: str, report_dates: List[str]) -> List[Dict[str, Any]]:
all_statements: List[Dict[str, Any]] = []
for date in report_dates:
logger.info(f"Fetching financial statements for {stock_code}, report date: {date}")
try:
bs_rows, ic_rows, cf_rows = await asyncio.gather(
self._query(
api_name="balancesheet",
params={"ts_code": stock_code, "period": date, "report_type": 1},
),
self._query(
api_name="income",
params={"ts_code": stock_code, "period": date, "report_type": 1},
),
self._query(
api_name="cashflow",
params={"ts_code": stock_code, "period": date, "report_type": 1},
)
)
if not bs_rows and not ic_rows and not cf_rows:
logger.warning(f"No financial statements components found from Tushare for {stock_code} on {date}")
continue
merged: Dict[str, Any] = {"ts_code": stock_code, "end_date": date}
bs_data = bs_rows[0] if bs_rows else {}
ic_data = ic_rows[0] if ic_rows else {}
cf_data = cf_rows[0] if cf_rows else {}
merged.update(bs_data)
merged.update(ic_data)
merged.update(cf_data)
merged["end_date"] = merged.get("end_date") or merged.get("period") or date
logger.debug(f"Merged statement for {date} has keys: {list(merged.keys())}")
all_statements.append(merged)
except Exception as e:
logger.error(f"Tushare get_financial_statement failed for {stock_code} on {date}: {e}")
continue
logger.info(f"Successfully fetched {len(all_statements)} statement(s) for {stock_code}.")
return all_statements