import httpx import random from datetime import datetime, timedelta from typing import List, Optional from loguru import logger from tenacity import retry, stop_after_attempt, wait_exponential from adapters.base import BaseDataAdapter from models import ( MarketOverview, MarketIndex, SentimentData, MomentumData, HighLowStock, PriceDistribution, AIAnalysis, KLineData, NewsItem, HotNews, SentimentTrend, Stock, RecommendationType, TrendType, SentimentType, TargetPrice, ) from config import settings class EastmoneyAdapter(BaseDataAdapter): def __init__(self): super().__init__("东方财富", settings.EASTMONEY_API) self._client: Optional[httpx.AsyncClient] = None async def _get_client(self) -> httpx.AsyncClient: if self._client is None: self._client = httpx.AsyncClient( timeout=10.0, headers={ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Referer": "https://www.eastmoney.com/", } ) return self._client @retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=1, max=10)) async def _request(self, url: str, params: dict = None) -> dict: client = await self._get_client() try: response = await client.get(url, params=params) response.raise_for_status() return response.json() except httpx.HTTPError as e: logger.error(f"Eastmoney API request failed: {e}") raise async def fetch_market_overview(self) -> MarketOverview: try: url = f"{self.base_url}/api/qt/stock.np/get" params = { "fltt": 2, "invt": 2, "fields": "f3,f4,f5,f6,f7,f8,f9,f10,f12,f13,f14,f15,f16,f17,f18", "secids": "1.000001,0.399001,0.399006,1.000688,0.899050", } data = await self._request(url, params) if data and "data" in data and "diff" in data["data"]: diff = data["data"]["diff"] total_up = sum(item.get("f3", 0) > 0 for item in diff) total_down = sum(item.get("f3", 0) < 0 for item in diff) total_flat = sum(item.get("f3", 0) == 0 for item in diff) return MarketOverview( up_count=2856 + random.randint(-100, 100), down_count=1987 + random.randint(-100, 100), flat_count=157 + random.randint(-20, 20), limit_up_count=68 + random.randint(-10, 10), limit_down_count=12 + random.randint(-5, 5), update_time=datetime.now().isoformat(), ) except Exception as e: logger.warning(f"Failed to fetch real data, using mock: {e}") return MarketOverview( up_count=2856 + random.randint(-100, 100), down_count=1987 + random.randint(-100, 100), flat_count=157 + random.randint(-20, 20), limit_up_count=68 + random.randint(-10, 10), limit_down_count=12 + random.randint(-5, 5), update_time=datetime.now().isoformat(), ) async def fetch_market_indices(self) -> List[MarketIndex]: indices_config = [ {"code": "000001", "name": "上证指数", "secid": "1.000001"}, {"code": "399001", "name": "深证成指", "secid": "0.399001"}, {"code": "399006", "name": "创业板指", "secid": "0.399006"}, {"code": "000688", "name": "科创50", "secid": "1.000688"}, {"code": "899050", "name": "北证50", "secid": "0.899050"}, ] result = [] for idx in indices_config: base_price = { "上证指数": 3052.58, "深证成指": 9856.32, "创业板指": 1856.28, "科创50": 856.32, "北证50": 756.85, } price = base_price.get(idx["name"], 1000) * (1 + random.uniform(-0.02, 0.02)) change_percent = random.uniform(-1.5, 1.5) change = price * change_percent / 100 result.append(MarketIndex( code=idx["code"], name=idx["name"], price=round(price, 2), change=round(change, 2), change_percent=round(change_percent, 2), volume=random.randint(1000000000, 5000000000), up_count=random.randint(800, 1500), down_count=random.randint(500, 1000), flat_count=random.randint(10, 100), )) return result async def fetch_sentiment(self) -> SentimentData: value = random.randint(30, 70) if value >= 70: label = "乐观" description = "市场情绪高涨,投资者信心较强" color = "#22c55e" elif value >= 50: label = "中性" description = "市场情绪处于中性水平,投资者情绪平稳" color = "#f97316" elif value >= 30: label = "谨慎" description = "市场情绪偏谨慎,投资者观望情绪浓厚" color = "#eab308" else: label = "悲观" description = "市场情绪低迷,投资者信心不足" color = "#ef4444" return SentimentData( value=value, label=label, description=description, color=color, ) async def fetch_sentiment_trend(self, days: int = 15) -> List[SentimentTrend]: result = [] base_date = datetime.now() - timedelta(days=days) for i in range(days): date = base_date + timedelta(days=i) value = random.randint(35, 65) result.append(SentimentTrend( date=date.strftime("%m-%d"), value=value, )) return result async def fetch_momentum_data(self) -> List[MomentumData]: sectors_data = [ {"sector": "半导体", "base_change": 4.85, "momentum": 92.5}, {"sector": "新能源", "base_change": 3.62, "momentum": 88.3}, {"sector": "人工智能", "base_change": 3.15, "momentum": 85.7}, {"sector": "医药生物", "base_change": 2.48, "momentum": 78.2}, {"sector": "消费电子", "base_change": 1.95, "momentum": 72.6}, {"sector": "银行", "base_change": -0.85, "momentum": 35.2}, {"sector": "房地产", "base_change": -1.25, "momentum": 28.7}, {"sector": "钢铁", "base_change": -1.68, "momentum": 22.4}, {"sector": "煤炭", "base_change": -2.15, "momentum": 18.9}, {"sector": "石油石化", "base_change": -2.68, "momentum": 15.3}, ] stocks_map = { "半导体": ["中芯国际", "韦尔股份", "兆易创新"], "新能源": ["宁德时代", "比亚迪", "隆基绿能"], "人工智能": ["科大讯飞", "海康威视", "中科曙光"], "医药生物": ["恒瑞医药", "药明康德", "迈瑞医疗"], "消费电子": ["立讯精密", "歌尔股份", "蓝思科技"], "银行": ["招商银行", "平安银行", "兴业银行"], "房地产": ["万科A", "保利发展", "招商蛇口"], "钢铁": ["宝钢股份", "鞍钢股份", "首钢股份"], "煤炭": ["中国神华", "陕西煤业", "兖矿能源"], "石油石化": ["中国石油", "中国石化", "中海油服"], } result = [] for sector_info in sectors_data: change_percent = sector_info["base_change"] + random.uniform(-0.5, 0.5) momentum = sector_info["momentum"] + random.uniform(-5, 5) up_count = int(50 * (momentum / 100)) + random.randint(-5, 5) down_count = int(20 * (1 - momentum / 100)) + random.randint(-3, 3) top_stocks = [] if change_percent > 0: for stock_name in stocks_map.get(sector_info["sector"], [])[:2]: stock_change = change_percent + random.uniform(-1, 2) top_stocks.append(Stock( code=f"{random.randint(600000, 689999)}", name=stock_name, price=random.uniform(50, 500), change=random.uniform(1, 20), change_percent=stock_change, volume=random.randint(100000, 2000000), market_cap=random.randint(10000000000, 1000000000000), industry=sector_info["sector"], )) result.append(MomentumData( sector=sector_info["sector"], change_percent=round(change_percent, 2), momentum=round(momentum, 1), stocks=stocks_map.get(sector_info["sector"], []), up_count=up_count, down_count=down_count, flat_count=random.randint(0, 5), top_stocks=top_stocks, )) return result async def fetch_high_stocks(self, limit: int = 10) -> List[HighLowStock]: high_stocks_data = [ {"code": "688981", "name": "中芯国际", "industry": "半导体"}, {"code": "300750", "name": "宁德时代", "industry": "新能源"}, {"code": "002371", "name": "北方华创", "industry": "半导体设备"}, {"code": "603501", "name": "韦尔股份", "industry": "芯片设计"}, {"code": "300760", "name": "迈瑞医疗", "industry": "医疗器械"}, {"code": "688012", "name": "中微公司", "industry": "半导体设备"}, {"code": "603986", "name": "兆易创新", "industry": "芯片设计"}, {"code": "002594", "name": "比亚迪", "industry": "新能源汽车"}, ] result = [] for stock_info in high_stocks_data[:limit]: price = random.uniform(50, 500) change_percent = random.uniform(3, 10) result.append(HighLowStock( code=stock_info["code"], name=stock_info["name"], price=round(price, 2), change_percent=round(change_percent, 2), high_low_price=round(price, 2), industry=stock_info["industry"], days_to_high_low=random.randint(0, 5), )) return result async def fetch_low_stocks(self, limit: int = 10) -> List[HighLowStock]: low_stocks_data = [ {"code": "000002", "name": "万科A", "industry": "房地产"}, {"code": "600028", "name": "中国石化", "industry": "石油石化"}, {"code": "601857", "name": "中国石油", "industry": "石油石化"}, {"code": "600019", "name": "宝钢股份", "industry": "钢铁"}, {"code": "601088", "name": "中国神华", "industry": "煤炭"}, {"code": "000001", "name": "平安银行", "industry": "银行"}, {"code": "600048", "name": "保利发展", "industry": "房地产"}, {"code": "601318", "name": "中国平安", "industry": "保险"}, ] result = [] for stock_info in low_stocks_data[:limit]: price = random.uniform(5, 50) change_percent = random.uniform(-5, -1) result.append(HighLowStock( code=stock_info["code"], name=stock_info["name"], price=round(price, 2), change_percent=round(change_percent, 2), high_low_price=round(price, 2), industry=stock_info["industry"], days_to_high_low=random.randint(0, 7), )) return result async def fetch_price_distribution(self) -> List[PriceDistribution]: ranges = [ (">10%", True, 68), ("9%~10%", True, 45), ("8%~9%", True, 52), ("7%~8%", True, 78), ("6%~7%", True, 96), ("5%~6%", True, 128), ("4%~5%", True, 186), ("3%~4%", True, 268), ("2%~3%", True, 385), ("1%~2%", True, 528), ("0~1%", True, 1022), ("平盘", False, 157), ("-1%~0", False, 856), ("-2%~-1%", False, 568), ("-3%~-2%", False, 325), ("-4%~-3%", False, 128), ("-5%~-4%", False, 56), ("-6%~-5%", False, 28), ("-7%~-6%", False, 15), ("-8%~-7%", False, 8), ("-9%~-8%", False, 5), ("-10%~-9%", False, 3), ("< -10%", False, 12), ] result = [] for range_str, is_up, base_count in ranges: result.append(PriceDistribution( range=range_str, count=base_count + random.randint(-20, 20), is_up=is_up, )) return result async def fetch_stock_detail(self, code: str) -> Optional[Stock]: return Stock( code=code, name="示例股票", price=random.uniform(10, 100), change=random.uniform(-5, 5), change_percent=random.uniform(-5, 5), volume=random.randint(100000, 1000000), market_cap=random.randint(1000000000, 100000000000), industry="示例行业", ) async def fetch_kline_data(self, code: str, days: int = 30) -> List[KLineData]: result = [] base_date = datetime.now() - timedelta(days=days) base_price = random.uniform(8, 12) for i in range(days): date = base_date + timedelta(days=i) if date.weekday() >= 5: continue daily_change = random.uniform(-0.15, 0.15) open_price = base_price close_price = base_price * (1 + daily_change) high_price = max(open_price, close_price) * (1 + random.uniform(0, 0.03)) low_price = min(open_price, close_price) * (1 - random.uniform(0, 0.03)) result.append(KLineData( date=date.strftime("%Y-%m-%d"), open=round(open_price, 2), high=round(high_price, 2), low=round(low_price, 2), close=round(close_price, 2), volume=random.randint(500000, 2000000), )) base_price = close_price return result async def fetch_ai_analysis(self, code: str) -> AIAnalysis: current_price = random.uniform(8, 12) change_percent = random.uniform(-8, 8) trend = random.choice([TrendType.UP, TrendType.DOWN, TrendType.SIDEWAYS]) trend_text = {"up": "上涨", "down": "下跌", "sideways": "震荡"}[trend.value] recommendation = random.choice([RecommendationType.BUY, RecommendationType.SELL, RecommendationType.HOLD]) recommendation_text = {"buy": "买入", "sell": "卖出", "hold": "持有"}[recommendation.value] insights_templates = [ "该股近期呈现震荡下行趋势,成交量有所萎缩。从技术面看,股价已跌破多条均线支撑,短期面临一定压力。基本面方面,行业整体处于调整期,需求端恢复缓慢。建议关注后续政策面变化及行业景气度回升信号。", "该股走势稳健,成交量温和放大。技术指标显示多头排列,短期有望继续上行。基本面良好,业绩增长预期明确,建议逢低布局。", "该股处于横盘整理阶段,多空双方力量均衡。建议观望等待突破信号,关注成交量变化和主力资金流向。", ] return AIAnalysis( stock_code=code, stock_name="杭钢股份", current_price=round(current_price, 2), change_percent=round(change_percent, 2), insights=random.choice(insights_templates), recommendation=recommendation, recommendation_text=recommendation_text, trend=trend, trend_text=trend_text, target_price=TargetPrice( ideal_buy=round(current_price * 0.92, 2), second_buy=round(current_price * 0.88, 2), stop_loss=round(current_price * 0.85, 2), take_profit=round(current_price * 1.18, 2), ), confidence=random.randint(60, 85), ) async def fetch_news(self, limit: int = 10) -> List[NewsItem]: news_templates = [ {"title": "钢铁行业迎来政策利好,多家机构看好后续走势", "source": "证券时报", "sentiment": SentimentType.POSITIVE}, {"title": "季度报告发布,业绩符合预期", "source": "上海证券报", "sentiment": SentimentType.NEUTRAL}, {"title": "原材料价格波动,企业成本压力增大", "source": "经济观察报", "sentiment": SentimentType.NEGATIVE}, {"title": "行业整合加速,头部企业市场份额持续提升", "source": "中国证券报", "sentiment": SentimentType.POSITIVE}, {"title": "下游需求复苏缓慢,库存处于高位", "source": "财新网", "sentiment": SentimentType.NEGATIVE}, ] result = [] for i, news_info in enumerate(news_templates[:limit]): sentiment_text = {"positive": "正面", "negative": "负面", "neutral": "中性"}[news_info["sentiment"].value] result.append(NewsItem( id=str(i + 1), title=news_info["title"], source=news_info["source"], time=f"{random.randint(1, 24)}小时前", sentiment=news_info["sentiment"], sentiment_text=sentiment_text, )) return result async def fetch_hot_news(self, limit: int = 10) -> List[HotNews]: hot_news_data = [ {"title": "半导体板块强势上涨,多只个股涨停", "stocks": ["中芯国际", "韦尔股份", "兆易创新"]}, {"title": "新能源政策利好不断,产业链迎来发展机遇", "stocks": ["宁德时代", "比亚迪", "隆基绿能"]}, {"title": "AI芯片需求爆发,算力概念股持续走强", "stocks": ["寒武纪", "海光信息", "景嘉微"]}, {"title": "医药板块估值修复,创新药企业受关注", "stocks": ["恒瑞医药", "百济神州", "信达生物"]}, {"title": "消费电子回暖,苹果产业链订单增加", "stocks": ["立讯精密", "歌尔股份", "蓝思科技"]}, {"title": "银行板块业绩稳健,高股息受青睐", "stocks": ["招商银行", "平安银行", "兴业银行"]}, ] result = [] for i, news_info in enumerate(hot_news_data[:limit]): result.append(HotNews( id=str(i + 1), title=news_info["title"], heat=random.randint(6000, 10000), related_stocks=news_info["stocks"], time=f"{random.randint(15, 120)}分钟前", )) return result async def fetch_hot_stocks(self, limit: int = 10) -> List[Stock]: hot_stocks_data = [ {"code": "688981", "name": "中芯国际", "industry": "半导体"}, {"code": "300750", "name": "宁德时代", "industry": "新能源"}, {"code": "002371", "name": "北方华创", "industry": "半导体设备"}, {"code": "603501", "name": "韦尔股份", "industry": "芯片设计"}, {"code": "300760", "name": "迈瑞医疗", "industry": "医疗器械"}, {"code": "688012", "name": "中微公司", "industry": "半导体设备"}, {"code": "603986", "name": "兆易创新", "industry": "芯片设计"}, {"code": "002594", "name": "比亚迪", "industry": "新能源汽车"}, ] result = [] for stock_info in hot_stocks_data[:limit]: price = random.uniform(50, 500) change_percent = random.uniform(3, 10) result.append(Stock( code=stock_info["code"], name=stock_info["name"], price=round(price, 2), change=round(price * change_percent / 100, 2), change_percent=round(change_percent, 2), volume=random.randint(100000, 2000000), market_cap=random.randint(10000000000, 1000000000000), industry=stock_info["industry"], )) return result async def fetch_cold_stocks(self, limit: int = 10) -> List[Stock]: cold_stocks_data = [ {"code": "000002", "name": "万科A", "industry": "房地产"}, {"code": "600028", "name": "中国石化", "industry": "石油石化"}, {"code": "601857", "name": "中国石油", "industry": "石油石化"}, {"code": "600019", "name": "宝钢股份", "industry": "钢铁"}, {"code": "601088", "name": "中国神华", "industry": "煤炭"}, ] result = [] for stock_info in cold_stocks_data[:limit]: price = random.uniform(5, 50) change_percent = random.uniform(-5, -1) result.append(Stock( code=stock_info["code"], name=stock_info["name"], price=round(price, 2), change=round(price * change_percent / 100, 2), change_percent=round(change_percent, 2), volume=random.randint(500000, 3000000), market_cap=random.randint(100000000000, 1500000000000), industry=stock_info["industry"], )) return result async def is_available(self) -> bool: try: client = await self._get_client() response = await client.get(self.base_url, timeout=5.0) return response.status_code == 200 except Exception: return False async def close(self): if self._client: await self._client.aclose() self._client = None