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
113 lines
4.7 KiB
Python
113 lines
4.7 KiB
Python
from .base import BaseDataProvider
|
|
from typing import Any, Dict, List, Optional
|
|
import finnhub
|
|
import pandas as pd
|
|
from datetime import datetime, timedelta
|
|
import asyncio
|
|
import logging
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
class FinnhubProvider(BaseDataProvider):
|
|
|
|
def _initialize(self):
|
|
if not self.token:
|
|
raise ValueError("Finnhub API key not provided.")
|
|
self.client = finnhub.Client(api_key=self.token)
|
|
|
|
async def get_stock_basic(self, stock_code: str) -> Optional[Dict[str, Any]]:
|
|
async def _fetch():
|
|
try:
|
|
profile = self.client.company_profile2(symbol=stock_code)
|
|
if not profile:
|
|
return None
|
|
|
|
# Normalize data
|
|
return {
|
|
"ts_code": stock_code,
|
|
"name": profile.get("name"),
|
|
"area": profile.get("country"),
|
|
"industry": profile.get("finnhubIndustry"),
|
|
"exchange": profile.get("exchange"),
|
|
"ipo_date": profile.get("ipo"),
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Finnhub get_stock_basic failed for {stock_code}: {e}")
|
|
return None
|
|
|
|
loop = asyncio.get_event_loop()
|
|
return await loop.run_in_executor(None, _fetch)
|
|
|
|
async def get_daily_price(self, stock_code: str, start_date: str, end_date: str) -> List[Dict[str, Any]]:
|
|
async def _fetch():
|
|
try:
|
|
start_ts = int(datetime.strptime(start_date, '%Y%m%d').timestamp())
|
|
end_ts = int(datetime.strptime(end_date, '%Y%m%d').timestamp())
|
|
|
|
res = self.client.stock_candles(stock_code, 'D', start_ts, end_ts)
|
|
if res.get('s') != 'ok':
|
|
return []
|
|
|
|
df = pd.DataFrame(res)
|
|
if df.empty:
|
|
return []
|
|
|
|
# Normalize data
|
|
df['trade_date'] = pd.to_datetime(df['t'], unit='s').dt.strftime('%Y%m%d')
|
|
df.rename(columns={
|
|
'o': 'open', 'h': 'high', 'l': 'low', 'c': 'close', 'v': 'vol'
|
|
}, inplace=True)
|
|
|
|
return df[['trade_date', 'open', 'high', 'low', 'close', 'vol']].to_dict('records')
|
|
|
|
except Exception as e:
|
|
logger.error(f"Finnhub get_daily_price failed for {stock_code}: {e}")
|
|
return []
|
|
|
|
loop = asyncio.get_event_loop()
|
|
return await loop.run_in_executor(None, _fetch)
|
|
|
|
async def get_financial_statements(self, stock_code: str, report_dates: List[str]) -> List[Dict[str, Any]]:
|
|
async def _fetch():
|
|
try:
|
|
# Finnhub provides financials as a whole, not by specific date ranges in one call
|
|
# We fetch all available and then filter.
|
|
# Note: 'freq' can be 'annual' or 'quarterly'. We'll use annual.
|
|
res = self.client.financials_reported(symbol=stock_code, freq='annual')
|
|
if not res or not res.get('data'):
|
|
return []
|
|
|
|
df = pd.DataFrame(res['data'])
|
|
|
|
# Filter by requested dates
|
|
years_to_fetch = {date[:4] for date in report_dates}
|
|
df = df[df['year'].astype(str).isin(years_to_fetch)]
|
|
|
|
# The data is deeply nested in 'report'. We need to extract and pivot it.
|
|
all_reports = []
|
|
for index, row in df.iterrows():
|
|
report_data = {'ts_code': stock_code, 'end_date': row['endDate']}
|
|
|
|
# Extract concepts from balance sheet, income statement, and cash flow
|
|
for item in row['report'].get('bs', []):
|
|
report_data[item['concept']] = item['value']
|
|
for item in row['report'].get('ic', []):
|
|
report_data[item['concept']] = item['value']
|
|
for item in row['report'].get('cf', []):
|
|
report_data[item['concept']] = item['value']
|
|
|
|
all_reports.append(report_data)
|
|
|
|
# Further normalization of keys would be needed here to match a common format
|
|
# e.g. 'AssetsTotal' -> 'total_assets'
|
|
# This is a complex task and depends on the desired final schema.
|
|
|
|
return all_reports
|
|
|
|
except Exception as e:
|
|
logger.error(f"Finnhub get_financial_statements failed for {stock_code}: {e}")
|
|
return []
|
|
|
|
loop = asyncio.get_event_loop()
|
|
return await loop.run_in_executor(None, _fetch)
|