You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

514 lines
22 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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