91d32c6ffa
* 增加更多的历史记录,修正部份数据获取错误 * 增加更多的历史记录,修正部份API数据获取错误,增加备用API --------- Co-authored-by: bathfire <>
538 lines
19 KiB
Python
538 lines
19 KiB
Python
"""
|
||
持仓管理器模块
|
||
|
||
提供持仓股票管理和批量分析功能
|
||
"""
|
||
|
||
import time
|
||
from typing import List, Dict, Optional, Tuple
|
||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||
from datetime import datetime
|
||
|
||
# 导入必要的模块
|
||
from portfolio_db import portfolio_db
|
||
|
||
|
||
class PortfolioManager:
|
||
"""持仓管理器类"""
|
||
|
||
def __init__(self, model="deepseek-chat"):
|
||
"""
|
||
初始化持仓管理器
|
||
|
||
Args:
|
||
model: AI模型(deepseek-chat 或 deepseek-reasoner)
|
||
"""
|
||
self.model = model
|
||
self.db = portfolio_db
|
||
|
||
# ==================== 持仓股票管理 ====================
|
||
|
||
def add_stock(self, code: str, name: str, cost_price: Optional[float] = None,
|
||
quantity: Optional[int] = None, note: str = "",
|
||
auto_monitor: bool = True) -> Tuple[bool, str, Optional[int]]:
|
||
"""
|
||
添加持仓股票
|
||
|
||
Args:
|
||
code: 股票代码
|
||
name: 股票名称
|
||
cost_price: 持仓成本价
|
||
quantity: 持仓数量
|
||
note: 备注
|
||
auto_monitor: 是否自动同步到监测
|
||
|
||
Returns:
|
||
(成功标志, 消息, 股票ID)
|
||
"""
|
||
try:
|
||
# 验证股票代码格式
|
||
code = code.strip().upper()
|
||
if not code:
|
||
return False, "股票代码不能为空", None
|
||
|
||
# 检查股票代码是否已存在
|
||
existing = self.db.get_stock_by_code(code)
|
||
if existing:
|
||
return False, f"股票代码 {code} 已存在", None
|
||
|
||
# 添加到数据库
|
||
stock_id = self.db.add_stock(code, name, cost_price, quantity, note, auto_monitor)
|
||
return True, f"添加持仓股票成功: {code} {name}", stock_id
|
||
|
||
except Exception as e:
|
||
return False, f"添加失败: {str(e)}", None
|
||
|
||
def update_stock(self, stock_id: int, **kwargs) -> Tuple[bool, str]:
|
||
"""
|
||
更新持仓股票信息
|
||
|
||
Args:
|
||
stock_id: 股票ID
|
||
**kwargs: 要更新的字段
|
||
|
||
Returns:
|
||
(成功标志, 消息)
|
||
"""
|
||
try:
|
||
success = self.db.update_stock(stock_id, **kwargs)
|
||
if success:
|
||
return True, "更新成功"
|
||
else:
|
||
return False, f"未找到股票ID: {stock_id}"
|
||
except Exception as e:
|
||
return False, f"更新失败: {str(e)}"
|
||
|
||
def delete_stock(self, stock_id: int) -> Tuple[bool, str]:
|
||
"""
|
||
删除持仓股票(级联删除分析历史)
|
||
|
||
Args:
|
||
stock_id: 股票ID
|
||
|
||
Returns:
|
||
(成功标志, 消息)
|
||
"""
|
||
try:
|
||
success = self.db.delete_stock(stock_id)
|
||
if success:
|
||
return True, "删除成功"
|
||
else:
|
||
return False, f"未找到股票ID: {stock_id}"
|
||
except Exception as e:
|
||
return False, f"删除失败: {str(e)}"
|
||
|
||
def get_stock(self, stock_id: int) -> Optional[Dict]:
|
||
"""获取单只持仓股票信息"""
|
||
return self.db.get_stock(stock_id)
|
||
|
||
def get_all_stocks(self, auto_monitor_only: bool = False) -> List[Dict]:
|
||
"""获取所有持仓股票列表"""
|
||
return self.db.get_all_stocks(auto_monitor_only)
|
||
|
||
def search_stocks(self, keyword: str) -> List[Dict]:
|
||
"""搜索持仓股票"""
|
||
return self.db.search_stocks(keyword)
|
||
|
||
def get_stock_count(self) -> int:
|
||
"""获取持仓股票总数"""
|
||
return self.db.get_stock_count()
|
||
|
||
# ==================== 单只股票分析 ====================
|
||
|
||
def analyze_single_stock(self, stock_code: str, period="1y",
|
||
selected_agents: List[str] = None) -> Dict:
|
||
"""
|
||
分析单只股票(复用app.py中的分析逻辑)
|
||
|
||
Args:
|
||
stock_code: 股票代码
|
||
period: 数据周期
|
||
selected_agents: 选中的分析师列表
|
||
|
||
Returns:
|
||
分析结果字典
|
||
"""
|
||
print(f"\n{'='*60}")
|
||
print(f"开始分析股票: {stock_code}")
|
||
print(f"{'='*60}\n")
|
||
|
||
try:
|
||
# 导入app.py中的分析函数
|
||
from app import analyze_single_stock_for_batch
|
||
|
||
# 构建分析师配置
|
||
if selected_agents is None:
|
||
enabled_analysts_config = {
|
||
'technical': True,
|
||
'fundamental': True,
|
||
'fund_flow': True,
|
||
'risk': True,
|
||
'sentiment': False,
|
||
'news': False
|
||
}
|
||
else:
|
||
enabled_analysts_config = {
|
||
'technical': 'technical' in selected_agents,
|
||
'fundamental': 'fundamental' in selected_agents,
|
||
'fund_flow': 'fund_flow' in selected_agents,
|
||
'risk': 'risk' in selected_agents,
|
||
'sentiment': 'sentiment' in selected_agents,
|
||
'news': 'news' in selected_agents
|
||
}
|
||
|
||
# 调用首页的分析函数
|
||
result = analyze_single_stock_for_batch(
|
||
symbol=stock_code,
|
||
period=period,
|
||
enabled_analysts_config=enabled_analysts_config,
|
||
selected_model=self.model
|
||
)
|
||
|
||
# 检查结果
|
||
if not result.get("success", False):
|
||
error_msg = result.get("error", "未知错误")
|
||
print(f"\n[ERROR] 分析失败: {error_msg}")
|
||
return {"success": False, "error": error_msg}
|
||
|
||
print(f"\n{'='*60}")
|
||
print(f"分析完成!")
|
||
print(f"{'='*60}\n")
|
||
|
||
return result
|
||
|
||
except Exception as e:
|
||
print(f"\n[ERROR] 分析失败: {str(e)}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
return {"success": False, "error": str(e)}
|
||
|
||
# ==================== 批量分析 ====================
|
||
|
||
def batch_analyze_sequential(self, stock_codes: List[str], period="1y",
|
||
selected_agents: List[str] = None,
|
||
progress_callback=None) -> Dict:
|
||
"""
|
||
顺序批量分析(逐只分析)
|
||
|
||
Args:
|
||
stock_codes: 股票代码列表
|
||
period: 数据周期
|
||
selected_agents: 选中的分析师列表
|
||
progress_callback: 进度回调函数 callback(current, total, code, status)
|
||
|
||
Returns:
|
||
批量分析结果字典
|
||
"""
|
||
print(f"\n{'='*60}")
|
||
print(f"开始批量分析 (顺序模式): {len(stock_codes)}只股票")
|
||
print(f"{'='*60}\n")
|
||
|
||
start_time = time.time()
|
||
results = []
|
||
failed = []
|
||
|
||
for i, code in enumerate(stock_codes, 1):
|
||
print(f"\n--- 分析进度: {i}/{len(stock_codes)} ---")
|
||
|
||
if progress_callback:
|
||
progress_callback(i, len(stock_codes), code, "analyzing")
|
||
|
||
try:
|
||
result = self.analyze_single_stock(code, period, selected_agents)
|
||
|
||
if result.get("success"):
|
||
results.append({
|
||
"code": code,
|
||
"result": result
|
||
})
|
||
if progress_callback:
|
||
progress_callback(i, len(stock_codes), code, "success")
|
||
else:
|
||
failed.append({
|
||
"code": code,
|
||
"error": result.get("error", "未知错误")
|
||
})
|
||
if progress_callback:
|
||
progress_callback(i, len(stock_codes), code, "failed")
|
||
|
||
except Exception as e:
|
||
print(f"[ERROR] 股票 {code} 分析失败: {str(e)}")
|
||
failed.append({
|
||
"code": code,
|
||
"error": str(e)
|
||
})
|
||
if progress_callback:
|
||
progress_callback(i, len(stock_codes), code, "error")
|
||
|
||
elapsed_time = time.time() - start_time
|
||
|
||
print(f"\n{'='*60}")
|
||
print(f"批量分析完成!")
|
||
print(f"成功: {len(results)}只, 失败: {len(failed)}只, 耗时: {elapsed_time:.1f}秒")
|
||
print(f"{'='*60}\n")
|
||
|
||
return {
|
||
"success": True,
|
||
"mode": "sequential",
|
||
"total": len(stock_codes),
|
||
"succeeded": len(results),
|
||
"failed": len(failed),
|
||
"results": results,
|
||
"failed_stocks": failed,
|
||
"elapsed_time": elapsed_time
|
||
}
|
||
|
||
def batch_analyze_parallel(self, stock_codes: List[str], period="1y",
|
||
selected_agents: List[str] = None,
|
||
max_workers: int = 3,
|
||
progress_callback=None) -> Dict:
|
||
"""
|
||
并行批量分析(多线程)
|
||
|
||
Args:
|
||
stock_codes: 股票代码列表
|
||
period: 数据周期
|
||
selected_agents: 选中的分析师列表
|
||
max_workers: 最大并发数(默认3)
|
||
progress_callback: 进度回调函数
|
||
|
||
Returns:
|
||
批量分析结果字典
|
||
"""
|
||
print(f"\n{'='*60}")
|
||
print(f"开始批量分析 (并行模式): {len(stock_codes)}只股票, 并发数: {max_workers}")
|
||
print(f"{'='*60}\n")
|
||
|
||
start_time = time.time()
|
||
results = []
|
||
failed = []
|
||
completed = 0
|
||
|
||
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
||
# 提交所有任务
|
||
future_to_code = {
|
||
executor.submit(self.analyze_single_stock, code, period, selected_agents): code
|
||
for code in stock_codes
|
||
}
|
||
|
||
# 处理完成的任务
|
||
for future in as_completed(future_to_code):
|
||
code = future_to_code[future]
|
||
completed += 1
|
||
|
||
try:
|
||
result = future.result()
|
||
|
||
if result.get("success"):
|
||
results.append({
|
||
"code": code,
|
||
"result": result
|
||
})
|
||
print(f"\n[{completed}/{len(stock_codes)}] {code} 分析完成")
|
||
if progress_callback:
|
||
progress_callback(completed, len(stock_codes), code, "success")
|
||
else:
|
||
failed.append({
|
||
"code": code,
|
||
"error": result.get("error", "未知错误")
|
||
})
|
||
print(f"\n[{completed}/{len(stock_codes)}] {code} 分析失败: {result.get('error')}")
|
||
if progress_callback:
|
||
progress_callback(completed, len(stock_codes), code, "failed")
|
||
|
||
except Exception as e:
|
||
failed.append({
|
||
"code": code,
|
||
"error": str(e)
|
||
})
|
||
print(f"\n[{completed}/{len(stock_codes)}] {code} 分析异常: {str(e)}")
|
||
if progress_callback:
|
||
progress_callback(completed, len(stock_codes), code, "error")
|
||
|
||
elapsed_time = time.time() - start_time
|
||
|
||
print(f"\n{'='*60}")
|
||
print(f"批量分析完成!")
|
||
print(f"成功: {len(results)}只, 失败: {len(failed)}只, 耗时: {elapsed_time:.1f}秒")
|
||
print(f"{'='*60}\n")
|
||
|
||
return {
|
||
"success": True,
|
||
"mode": "parallel",
|
||
"total": len(stock_codes),
|
||
"succeeded": len(results),
|
||
"failed": len(failed),
|
||
"results": results,
|
||
"failed_stocks": failed,
|
||
"elapsed_time": elapsed_time
|
||
}
|
||
|
||
def batch_analyze_portfolio(self, mode="sequential", period="1y",
|
||
selected_agents: List[str] = None,
|
||
max_workers: int = 3,
|
||
progress_callback=None) -> Dict:
|
||
"""
|
||
批量分析所有持仓股票
|
||
|
||
Args:
|
||
mode: 分析模式 ("sequential" 或 "parallel")
|
||
period: 数据周期
|
||
selected_agents: 选中的分析师列表
|
||
max_workers: 并行模式下的最大并发数(默认3)
|
||
progress_callback: 进度回调函数
|
||
|
||
Returns:
|
||
批量分析结果字典
|
||
"""
|
||
# 获取所有持仓股票
|
||
stocks = self.get_all_stocks()
|
||
|
||
if not stocks:
|
||
return {
|
||
"success": False,
|
||
"error": "没有持仓股票"
|
||
}
|
||
|
||
stock_codes = [stock['code'] for stock in stocks]
|
||
|
||
# 根据模式选择分析方法
|
||
if mode == "parallel":
|
||
return self.batch_analyze_parallel(stock_codes, period, selected_agents, max_workers, progress_callback)
|
||
else:
|
||
return self.batch_analyze_sequential(stock_codes, period, selected_agents, progress_callback)
|
||
|
||
# ==================== 分析结果保存 ====================
|
||
|
||
def save_analysis_results(self, analysis_results: Dict) -> List[int]:
|
||
"""
|
||
保存批量分析结果到数据库
|
||
|
||
Args:
|
||
analysis_results: 批量分析结果字典
|
||
|
||
Returns:
|
||
保存的分析记录ID列表
|
||
"""
|
||
saved_ids = []
|
||
|
||
if not analysis_results.get("success"):
|
||
print("[WARN] 分析未成功,跳过保存")
|
||
return saved_ids
|
||
|
||
for item in analysis_results.get("results", []):
|
||
code = item.get("code")
|
||
result = item.get("result", {})
|
||
|
||
# 获取持仓股票ID
|
||
stock = self.db.get_stock_by_code(code)
|
||
if not stock:
|
||
print(f"[WARN] 未找到持仓股票: {code},跳过保存")
|
||
continue
|
||
|
||
stock_id = stock['id']
|
||
|
||
# 提取分析结果关键信息
|
||
final_decision = result.get("final_decision", {})
|
||
stock_info = result.get("stock_info", {})
|
||
|
||
# 使用正确的字段名
|
||
rating = final_decision.get("rating", "持有")
|
||
# 确保信心度为float类型,避免Arrow序列化错误
|
||
confidence_raw = final_decision.get("confidence_level", 5.0)
|
||
try:
|
||
confidence = float(confidence_raw)
|
||
# 确保信心度在合理范围内
|
||
if confidence < 0:
|
||
confidence = 0.0
|
||
elif confidence > 10:
|
||
confidence = 10.0
|
||
except (ValueError, TypeError):
|
||
# 如果转换失败,使用默认值
|
||
confidence = 5.0
|
||
current_price = stock_info.get("current_price", 0.0)
|
||
target_price_str = final_decision.get("target_price", "")
|
||
entry_range = final_decision.get("entry_range", "")
|
||
take_profit_str = final_decision.get("take_profit", "")
|
||
stop_loss_str = final_decision.get("stop_loss", "")
|
||
|
||
# 解析目标价格
|
||
import re
|
||
target_price = None
|
||
if target_price_str:
|
||
try:
|
||
numbers = re.findall(r'\d+\.?\d*', str(target_price_str))
|
||
if numbers:
|
||
target_price = float(numbers[0])
|
||
except:
|
||
pass
|
||
|
||
# 解析进场区间
|
||
entry_min, entry_max = None, None
|
||
if entry_range and isinstance(entry_range, str) and "-" in entry_range:
|
||
try:
|
||
parts = entry_range.split("-")
|
||
entry_min = float(parts[0].strip())
|
||
entry_max = float(parts[1].strip())
|
||
except:
|
||
pass
|
||
|
||
# 解析止盈止损
|
||
take_profit, stop_loss = None, None
|
||
if take_profit_str:
|
||
try:
|
||
numbers = re.findall(r'\d+\.?\d*', str(take_profit_str))
|
||
if numbers:
|
||
take_profit = float(numbers[0])
|
||
except:
|
||
pass
|
||
|
||
if stop_loss_str:
|
||
try:
|
||
numbers = re.findall(r'\d+\.?\d*', str(stop_loss_str))
|
||
if numbers:
|
||
stop_loss = float(numbers[0])
|
||
except:
|
||
pass
|
||
|
||
# 生成摘要(使用advice或summary字段)
|
||
summary = final_decision.get("advice", final_decision.get("summary", ""))[:500] # 限制长度
|
||
|
||
try:
|
||
# 保存到数据库
|
||
analysis_id = self.db.save_analysis(
|
||
stock_id, rating, confidence, current_price, target_price,
|
||
entry_min, entry_max, take_profit, stop_loss, summary
|
||
)
|
||
saved_ids.append(analysis_id)
|
||
|
||
except Exception as e:
|
||
print(f"[ERROR] 保存分析结果失败 ({code}): {str(e)}")
|
||
|
||
print(f"\n[OK] 保存分析结果: {len(saved_ids)}条记录")
|
||
return saved_ids
|
||
|
||
# ==================== 分析历史查询 ====================
|
||
|
||
def get_analysis_history(self, stock_id: int, limit: int = 10) -> List[Dict]:
|
||
"""获取股票分析历史"""
|
||
return self.db.get_analysis_history(stock_id, limit)
|
||
|
||
def get_latest_analysis(self, stock_id: int) -> Optional[Dict]:
|
||
"""获取最新一次分析"""
|
||
return self.db.get_latest_analysis(stock_id)
|
||
|
||
def get_all_latest_analysis(self) -> List[Dict]:
|
||
"""获取所有持仓股票的最新分析"""
|
||
return self.db.get_all_latest_analysis()
|
||
|
||
def get_rating_changes(self, stock_id: int, days: int = 30) -> List[Tuple]:
|
||
"""获取评级变化"""
|
||
return self.db.get_rating_changes(stock_id, days)
|
||
|
||
|
||
# 创建全局实例
|
||
portfolio_manager = PortfolioManager()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
# 测试代码
|
||
print("="*60)
|
||
print("持仓管理器测试")
|
||
print("="*60)
|
||
|
||
manager = PortfolioManager()
|
||
|
||
# 测试添加持仓
|
||
success, msg, stock_id = manager.add_stock("000001", "平安银行", 12.5, 1000, "测试持仓")
|
||
print(f"\n添加持仓: {msg}")
|
||
|
||
# 测试获取所有持仓
|
||
stocks = manager.get_all_stocks()
|
||
print(f"\n持仓数量: {len(stocks)}")
|
||
for stock in stocks:
|
||
print(f" {stock['code']} {stock['name']} - 成本:{stock['cost_price']}, 数量:{stock['quantity']}")
|
||
|
||
print("\n[OK] 持仓管理器测试完成")
|
||
|