mirror of
https://github.com/ZhuLinsen/daily_stock_analysis
synced 2026-09-20 10:53:33 +08:00
* feat(web): add FastAPI & React web connect * feat: add FastAPI package * feat: system base struct * feat: frontend base struct * feat: 基础分析接口,历史记录接口 * fix: 使用正确的 FastAPI 方法定义,避免线程阻塞 * fix: 对接历史报告页、详情页 * fix: 修复接口问题 * fix: 网络配置 允许公网访问 跨域配置 * fix: 页面打包 & 提供 Server 静态访问 * fix: 优化dock栏样式 * fix: 优化图标样式 * fix: 修改部分配色 * fix: 删除垃圾文档 * fix: 删除垃圾代码 * fix: 驼峰转换工具 * fix: 历史记录列表滚动加载(分页) * feat: 显示分析中任务 * fix: 调整布局 * fix: 优化 Market Sentiment 组件动效 * fix: 历史列表组件bug * fix: FastAPI 日志配置 * feat: 新闻历史接口 * feat: 资讯列表 * feat: 优化布局 * fix: 修复页面元素变宽问题 * fix: 中文标题 * fix: 任务列表状态显示问题 * fix: 抽取日志配置 * fix: 优化报告价格显示 * fix: 修复编译错误 * fix: FastAPI 启动提取到 main.py * fix: 补充新web-ui启动文档 * fix: 默认发送通知 * fix: FastAPI 模式适配 docker * fix: FastAPI 模式文档 * Update api/v1/endpoints/stocks.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * fix: package-lock.json * fix: 修改错别字 * fix: 更新文档说明 * fix: 更新文档说明 --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
187 lines
6.4 KiB
Python
187 lines
6.4 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""
|
||
===================================
|
||
股票数据服务层
|
||
===================================
|
||
|
||
职责:
|
||
1. 封装股票数据获取逻辑
|
||
2. 提供实时行情和历史数据接口
|
||
"""
|
||
|
||
import logging
|
||
from datetime import datetime, timedelta
|
||
from typing import Optional, Dict, Any, List
|
||
|
||
from src.repositories.stock_repo import StockRepository
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class StockService:
|
||
"""
|
||
股票数据服务
|
||
|
||
封装股票数据获取的业务逻辑
|
||
"""
|
||
|
||
def __init__(self):
|
||
"""初始化股票数据服务"""
|
||
self.repo = StockRepository()
|
||
|
||
def get_realtime_quote(self, stock_code: str) -> Optional[Dict[str, Any]]:
|
||
"""
|
||
获取股票实时行情
|
||
|
||
Args:
|
||
stock_code: 股票代码
|
||
|
||
Returns:
|
||
实时行情数据字典
|
||
"""
|
||
try:
|
||
# 调用数据获取器获取实时行情
|
||
from data_provider.base import DataFetcherManager
|
||
|
||
manager = DataFetcherManager()
|
||
quote = manager.get_realtime_quote(stock_code)
|
||
|
||
if quote is None:
|
||
logger.warning(f"获取 {stock_code} 实时行情失败")
|
||
return None
|
||
|
||
# UnifiedRealtimeQuote 是 dataclass,使用 getattr 安全访问字段
|
||
# 字段映射: UnifiedRealtimeQuote -> API 响应
|
||
# - code -> stock_code
|
||
# - name -> stock_name
|
||
# - price -> current_price
|
||
# - change_amount -> change
|
||
# - change_pct -> change_percent
|
||
# - open_price -> open
|
||
# - high -> high
|
||
# - low -> low
|
||
# - pre_close -> prev_close
|
||
# - volume -> volume
|
||
# - amount -> amount
|
||
return {
|
||
"stock_code": getattr(quote, "code", stock_code),
|
||
"stock_name": getattr(quote, "name", None),
|
||
"current_price": getattr(quote, "price", 0.0) or 0.0,
|
||
"change": getattr(quote, "change_amount", None),
|
||
"change_percent": getattr(quote, "change_pct", None),
|
||
"open": getattr(quote, "open_price", None),
|
||
"high": getattr(quote, "high", None),
|
||
"low": getattr(quote, "low", None),
|
||
"prev_close": getattr(quote, "pre_close", None),
|
||
"volume": getattr(quote, "volume", None),
|
||
"amount": getattr(quote, "amount", None),
|
||
"update_time": datetime.now().isoformat(),
|
||
}
|
||
|
||
except ImportError:
|
||
logger.warning("DataFetcherManager 未找到,使用占位数据")
|
||
return self._get_placeholder_quote(stock_code)
|
||
except Exception as e:
|
||
logger.error(f"获取实时行情失败: {e}", exc_info=True)
|
||
return None
|
||
|
||
def get_history_data(
|
||
self,
|
||
stock_code: str,
|
||
period: str = "daily",
|
||
days: int = 30
|
||
) -> Dict[str, Any]:
|
||
"""
|
||
获取股票历史行情
|
||
|
||
Args:
|
||
stock_code: 股票代码
|
||
period: K 线周期 (daily/weekly/monthly)
|
||
days: 获取天数
|
||
|
||
Returns:
|
||
历史行情数据字典
|
||
|
||
Raises:
|
||
ValueError: 当 period 不是 daily 时抛出(weekly/monthly 暂未实现)
|
||
"""
|
||
# 验证 period 参数,只支持 daily
|
||
if period != "daily":
|
||
raise ValueError(
|
||
f"暂不支持 '{period}' 周期,目前仅支持 'daily'。"
|
||
"weekly/monthly 聚合功能将在后续版本实现。"
|
||
)
|
||
|
||
try:
|
||
# 调用数据获取器获取历史数据
|
||
from data_provider.base import DataFetcherManager
|
||
|
||
manager = DataFetcherManager()
|
||
df, source = manager.get_daily_data(stock_code, days=days)
|
||
|
||
if df is None or df.empty:
|
||
logger.warning(f"获取 {stock_code} 历史数据失败")
|
||
return {"stock_code": stock_code, "period": period, "data": []}
|
||
|
||
# 获取股票名称
|
||
stock_name = manager.get_stock_name(stock_code)
|
||
|
||
# 转换为响应格式
|
||
data = []
|
||
for _, row in df.iterrows():
|
||
date_val = row.get("date")
|
||
if hasattr(date_val, "strftime"):
|
||
date_str = date_val.strftime("%Y-%m-%d")
|
||
else:
|
||
date_str = str(date_val)
|
||
|
||
data.append({
|
||
"date": date_str,
|
||
"open": float(row.get("open", 0)),
|
||
"high": float(row.get("high", 0)),
|
||
"low": float(row.get("low", 0)),
|
||
"close": float(row.get("close", 0)),
|
||
"volume": float(row.get("volume", 0)) if row.get("volume") else None,
|
||
"amount": float(row.get("amount", 0)) if row.get("amount") else None,
|
||
"change_percent": float(row.get("pct_chg", 0)) if row.get("pct_chg") else None,
|
||
})
|
||
|
||
return {
|
||
"stock_code": stock_code,
|
||
"stock_name": stock_name,
|
||
"period": period,
|
||
"data": data,
|
||
}
|
||
|
||
except ImportError:
|
||
logger.warning("DataFetcherManager 未找到,返回空数据")
|
||
return {"stock_code": stock_code, "period": period, "data": []}
|
||
except Exception as e:
|
||
logger.error(f"获取历史数据失败: {e}", exc_info=True)
|
||
return {"stock_code": stock_code, "period": period, "data": []}
|
||
|
||
def _get_placeholder_quote(self, stock_code: str) -> Dict[str, Any]:
|
||
"""
|
||
获取占位行情数据(用于测试)
|
||
|
||
Args:
|
||
stock_code: 股票代码
|
||
|
||
Returns:
|
||
占位行情数据
|
||
"""
|
||
return {
|
||
"stock_code": stock_code,
|
||
"stock_name": f"股票{stock_code}",
|
||
"current_price": 0.0,
|
||
"change": None,
|
||
"change_percent": None,
|
||
"open": None,
|
||
"high": None,
|
||
"low": None,
|
||
"prev_close": None,
|
||
"volume": None,
|
||
"amount": None,
|
||
"update_time": datetime.now().isoformat(),
|
||
}
|