增加定时分析功能

This commit is contained in:
oficcejo
2025-10-20 09:18:44 +08:00
parent f7d81a1469
commit 1bddf393e0
13 changed files with 3132 additions and 1 deletions
+771
View File
@@ -0,0 +1,771 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
持仓管理UI模块
提供持仓股票的增删改查、批量分析、定时任务管理界面
"""
import streamlit as st
import pandas as pd
from datetime import datetime
from typing import List, Dict
import time
from portfolio_manager import portfolio_manager
from portfolio_scheduler import portfolio_scheduler
def display_portfolio_manager():
"""显示持仓管理主界面"""
st.markdown("## 📊 持仓定时分析")
st.markdown("---")
# 创建标签页
tab1, tab2, tab3, tab4 = st.tabs([
"📝 持仓管理",
"🔄 批量分析",
"⏰ 定时任务",
"📈 分析历史"
])
with tab1:
display_portfolio_stocks()
with tab2:
display_batch_analysis()
with tab3:
display_scheduler_management()
with tab4:
display_analysis_history()
def display_portfolio_stocks():
"""显示持仓股票列表和管理"""
st.markdown("### 📝 持仓股票管理")
# 添加新股票表单
with st.expander(" 添加持仓股票", expanded=False):
display_add_stock_form()
# 获取所有持仓股票
stocks = portfolio_manager.get_all_stocks()
if not stocks:
st.info("暂无持仓股票,请添加股票代码开始管理。")
return
# 显示统计
col1, col2, col3 = st.columns(3)
with col1:
st.metric("持仓股票数", len(stocks))
with col2:
auto_monitor_count = sum(1 for s in stocks if s.get("auto_monitor"))
st.metric("启用自动监测", auto_monitor_count)
with col3:
total_cost = sum(
s.get("cost_price", 0) * s.get("quantity", 0)
for s in stocks
if s.get("cost_price") and s.get("quantity")
)
st.metric("总持仓成本", f"¥{total_cost:,.2f}")
st.markdown("---")
# 显示股票列表(卡片式布局)
for stock in stocks:
display_stock_card(stock)
def display_stock_card(stock: Dict):
"""显示单个股票卡片"""
code = stock.get("code", "")
name = stock.get("name", "")
cost_price = stock.get("cost_price")
quantity = stock.get("quantity")
note = stock.get("note", "")
auto_monitor = stock.get("auto_monitor", True)
created_at = stock.get("created_at", "")
# 创建卡片
with st.container():
col1, col2, col3, col4 = st.columns([3, 2, 2, 2])
with col1:
st.markdown(f"**{code}** {name}")
if note:
st.caption(f"备注: {note}")
with col2:
if cost_price and quantity:
st.write(f"成本: ¥{cost_price:.2f}")
st.caption(f"数量: {quantity}")
else:
st.caption("未设置持仓")
with col3:
if auto_monitor:
st.success("🔔 自动监测")
else:
st.info("🔕 不监测")
with col4:
col_edit, col_del = st.columns(2)
with col_edit:
if st.button("✏️", key=f"edit_{code}", help="编辑"):
st.session_state[f"editing_{code}"] = True
st.rerun()
with col_del:
if st.button("🗑️", key=f"del_{code}", help="删除"):
portfolio_manager.delete_stock(code)
st.success(f"已删除 {code}")
time.sleep(0.5)
st.rerun()
# 编辑表单(如果处于编辑状态)
if st.session_state.get(f"editing_{code}"):
with st.form(key=f"edit_form_{code}"):
st.markdown(f"#### 编辑 {code}")
col_a, col_b = st.columns(2)
with col_a:
new_cost = st.number_input(
"成本价",
value=cost_price if cost_price else 0.0,
min_value=0.0,
step=0.01
)
new_quantity = st.number_input(
"持仓数量",
value=quantity if quantity else 0,
min_value=0,
step=100
)
with col_b:
new_note = st.text_area("备注", value=note, height=80)
new_auto_monitor = st.checkbox("自动同步到监测", value=auto_monitor)
col_submit, col_cancel = st.columns(2)
with col_submit:
if st.form_submit_button("保存", type="primary"):
portfolio_manager.update_stock(
code,
cost_price=new_cost if new_cost > 0 else None,
quantity=new_quantity if new_quantity > 0 else None,
note=new_note,
auto_monitor=new_auto_monitor
)
del st.session_state[f"editing_{code}"]
st.success("更新成功!")
time.sleep(0.5)
st.rerun()
with col_cancel:
if st.form_submit_button("取消"):
del st.session_state[f"editing_{code}"]
st.rerun()
st.markdown("---")
def display_add_stock_form():
"""显示添加股票表单"""
with st.form(key="add_stock_form"):
col1, col2 = st.columns(2)
with col1:
code = st.text_input(
"股票代码*",
placeholder="例如: 600519.SH 或 000001.SZ",
help="必填,格式:代码.市场(SH/SZ/HK/US"
)
name = st.text_input(
"股票名称",
placeholder="例如: 贵州茅台",
help="可选,留空将自动获取"
)
with col2:
cost_price = st.number_input(
"成本价",
min_value=0.0,
step=0.01,
help="可选,用于计算收益"
)
quantity = st.number_input(
"持仓数量",
min_value=0,
step=100,
help="可选,单位:股"
)
note = st.text_area("备注", height=80, placeholder="可选,记录买入理由等信息")
auto_monitor = st.checkbox("分析后自动同步到监测", value=True)
if st.form_submit_button(" 添加股票", type="primary"):
if not code:
st.error("请输入股票代码")
else:
try:
portfolio_manager.add_stock(
code=code.strip().upper(),
name=name.strip() if name else None,
cost_price=cost_price if cost_price > 0 else None,
quantity=quantity if quantity > 0 else None,
note=note.strip() if note else None,
auto_monitor=auto_monitor
)
st.success(f"✅ 已添加 {code} 到持仓列表")
time.sleep(0.5)
st.rerun()
except Exception as e:
st.error(f"添加失败: {str(e)}")
def display_batch_analysis():
"""显示批量分析功能"""
st.markdown("### 🔄 批量分析持仓股票")
stocks = portfolio_manager.get_all_stocks()
if not stocks:
st.warning("暂无持仓股票,请先添加股票。")
return
# 分析选项
col1, col2, col3 = st.columns(3)
with col1:
st.metric("持仓股票数", len(stocks))
with col2:
analysis_mode = st.selectbox(
"分析模式",
options=["sequential", "parallel"],
format_func=lambda x: "顺序分析" if x == "sequential" else "并行分析",
help="顺序分析较慢但稳定,并行分析更快但消耗更多资源"
)
with col3:
if analysis_mode == "parallel":
max_workers = st.number_input(
"并行线程数",
min_value=2,
max_value=10,
value=3,
help="同时分析的股票数量"
)
else:
max_workers = 1
st.markdown("---")
# 同步和通知选项
col_a, col_b = st.columns(2)
with col_a:
auto_sync = st.checkbox(
"自动同步到监测",
value=True,
help="分析完成后自动将评级结果同步到实时监测列表"
)
with col_b:
send_notification = st.checkbox(
"发送完成通知",
value=True,
help="通过邮件或Webhook发送分析完成通知"
)
# 立即分析按钮
if st.button("🚀 立即开始分析", type="primary", use_container_width=True):
with st.spinner("正在批量分析持仓股票..."):
# 显示进度
progress_bar = st.progress(0)
status_text = st.empty()
# 执行批量分析
try:
# 定义进度回调函数
def update_progress(current, total, code, status):
progress_bar.progress(current / total)
status_map = {
"analyzing": "正在分析",
"success": "✅ 完成",
"failed": "❌ 失败",
"error": "⚠️ 错误"
}
status_text.text(f"{status_map.get(status, '处理中')} {code} ({current}/{total})")
result = portfolio_manager.batch_analyze_portfolio(
mode=analysis_mode,
max_workers=max_workers,
progress_callback=update_progress
)
# 清除进度显示
progress_bar.empty()
status_text.empty()
# 显示结果
st.success(f"✅ 批量分析完成!")
col_r1, col_r2, col_r3, col_r4 = st.columns(4)
with col_r1:
st.metric("总计", result.get("total", 0))
with col_r2:
st.metric("成功", result.get("succeeded", 0))
with col_r3:
st.metric("失败", result.get("failed", 0))
with col_r4:
st.metric("耗时", f"{result.get('elapsed_time', 0):.1f}")
# 保存分析结果到数据库
saved_ids = portfolio_manager.save_analysis_results(result)
st.info(f"💾 已保存 {len(saved_ids)} 条分析记录到数据库")
# 同步到监测
sync_result = None # 初始化同步结果
if auto_sync:
with st.spinner("正在同步到监测列表..."):
from monitor_db import monitor_db
# 准备同步数据
monitors_to_sync = []
for item in result.get("results", []):
# 检查分析是否成功
if not item.get("result", {}).get("success"):
continue
code = item["code"]
stock = portfolio_manager.db.get_stock_by_code(code)
# 只同步启用了自动监测的股票
if not stock or not stock.get("auto_monitor"):
continue
analysis_result = item["result"]
stock_info = analysis_result.get("stock_info", {})
final_decision = analysis_result.get("final_decision", {})
# 从final_decision中提取数据
rating = final_decision.get("rating", "持有")
entry_range = final_decision.get("entry_range", "")
take_profit_str = final_decision.get("take_profit", "")
stop_loss_str = final_decision.get("stop_loss", "")
# 解析进场区间(格式如"10.5-12.3"
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
# 解析止盈止损(提取数字)
import re
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
# 只有当所有必需字段都有效时才添加
if entry_min and entry_max and take_profit and stop_loss:
monitors_to_sync.append({
"code": code,
"name": stock_info.get("name", stock.get("name", "")),
"rating": rating,
"entry_min": entry_min,
"entry_max": entry_max,
"take_profit": take_profit,
"stop_loss": stop_loss
})
if monitors_to_sync:
sync_result = monitor_db.batch_add_or_update_monitors(monitors_to_sync)
st.info(f"📊 监测同步: 新增 {sync_result.get('added', 0)} 只, 更新 {sync_result.get('updated', 0)}")
else:
sync_result = {"added": 0, "updated": 0, "failed": 0, "total": 0}
st.info("📊 无需同步监测列表(无启用自动监测的股票)")
# 发送通知
if send_notification:
from notification_service import notification_service
notification_service.send_portfolio_analysis_notification(
result,
sync_result if auto_sync else None
)
st.info("✉️ 已发送完成通知")
# 显示详细结果
st.markdown("### 分析结果详情")
for item in result.get("results", []):
display_analysis_result_card(item)
except Exception as e:
st.error(f"批量分析失败: {str(e)}")
import traceback
st.code(traceback.format_exc())
def display_analysis_result_card(item: Dict):
"""显示单个分析结果卡片"""
code = item.get("code", "")
result = item.get("result", {})
# 检查分析是否成功
if result.get("success"):
final_decision = result.get("final_decision", {})
stock_info = result.get("stock_info", {})
# 使用正确的字段名
rating = final_decision.get("rating", "未知")
confidence = final_decision.get("confidence_level", "N/A")
target_price = final_decision.get("target_price", "N/A")
entry_range = final_decision.get("entry_range", "N/A")
take_profit = final_decision.get("take_profit", "N/A")
stop_loss = final_decision.get("stop_loss", "N/A")
# 评级颜色
if "强烈买入" in rating or "买入" in rating:
rating_color = "🟢"
elif "卖出" in rating:
rating_color = "🔴"
else:
rating_color = "🟡"
with st.expander(f"{rating_color} {code} {stock_info.get('name', '')} - {rating} (信心度: {confidence})"):
col1, col2 = st.columns(2)
with col1:
st.markdown("**进出场位置**")
st.write(f"进场区间: {entry_range}")
st.write(f"目标价: {target_price}")
with col2:
st.markdown("**风控位置**")
st.write(f"止盈位: {take_profit}")
st.write(f"止损位: {stop_loss}")
# 投资建议
advice = final_decision.get("advice", "")
if advice:
st.markdown("**投资建议**")
st.info(advice)
else:
# 分析失败
error = result.get("error", "未知错误")
with st.expander(f"🔴 {code} - 分析失败"):
st.error(f"错误: {error}")
def display_scheduler_management():
"""显示定时任务管理"""
st.markdown("### ⏰ 定时任务管理")
# 调度器状态
is_running = portfolio_scheduler.is_running()
schedule_times = portfolio_scheduler.get_schedule_times()
col1, col2, col3 = st.columns(3)
with col1:
if is_running:
st.success("🟢 调度器运行中")
else:
st.error("🔴 调度器已停止")
with col2:
st.info(f"⏰ 定时数量: {len(schedule_times)}")
with col3:
next_run = portfolio_scheduler.get_next_run_time()
if next_run:
st.info(f"⏭️ 下次运行: {next_run}")
else:
st.info("⏭️ 下次运行: 未设置")
st.markdown("---")
# 显示所有定时时间点
st.markdown("#### 📋 已配置的定时时间")
if schedule_times:
cols_per_row = 4
for i in range(0, len(schedule_times), cols_per_row):
cols = st.columns(cols_per_row)
for j, col in enumerate(cols):
idx = i + j
if idx < len(schedule_times):
time_str = schedule_times[idx]
with col:
col_time, col_del = st.columns([3, 1])
with col_time:
st.info(f"{time_str}")
with col_del:
if st.button("🗑️", key=f"del_time_{idx}", help="删除"):
if len(schedule_times) > 1:
portfolio_scheduler.remove_schedule_time(time_str)
st.success(f"已删除 {time_str}")
time.sleep(0.3)
st.rerun()
else:
st.error("至少保留一个定时时间")
else:
st.warning("暂无定时配置")
# 添加新的定时时间
with st.expander(" 添加定时时间", expanded=False):
col_input, col_add = st.columns([3, 1])
with col_input:
new_time = st.time_input(
"选择时间",
value=datetime.strptime("15:05", "%H:%M").time(),
help="添加新的每日分析时间"
)
with col_add:
st.write("") # 占位,对齐按钮
st.write("")
if st.button(" 添加", type="primary", use_container_width=True):
time_str = new_time.strftime("%H:%M")
if portfolio_scheduler.add_schedule_time(time_str):
st.success(f"已添加 {time_str}")
time.sleep(0.3)
st.rerun()
else:
st.warning(f"{time_str} 已存在")
st.markdown("---")
# 任务配置
with st.form(key="scheduler_config_form"):
st.markdown("#### 分析配置")
col_a, col_b = st.columns(2)
with col_a:
analysis_mode = st.selectbox(
"分析模式",
options=["sequential", "parallel"],
format_func=lambda x: "顺序分析" if x == "sequential" else "并行分析",
index=0 if portfolio_scheduler.analysis_mode == "sequential" else 1
)
with col_b:
max_workers = st.number_input(
"并行线程数",
min_value=2,
max_value=10,
value=portfolio_scheduler.max_workers,
disabled=(analysis_mode == "sequential"),
help="仅在并行模式下生效"
)
auto_sync_monitor = st.checkbox(
"自动同步到监测",
value=portfolio_scheduler.auto_monitor_sync,
help="分析完成后自动将结果同步到实时监测列表"
)
send_notification = st.checkbox(
"发送完成通知",
value=portfolio_scheduler.notification_enabled,
help="通过邮件或Webhook发送分析结果"
)
col_update, col_reset = st.columns(2)
with col_update:
if st.form_submit_button("💾 更新配置", type="primary"):
portfolio_scheduler.update_config(
analysis_mode=analysis_mode,
max_workers=max_workers if analysis_mode == "parallel" else 1,
auto_sync_monitor=auto_sync_monitor,
send_notification=send_notification
)
st.success("配置已更新!")
time.sleep(0.5)
st.rerun()
with col_reset:
if st.form_submit_button("🔄 恢复默认"):
portfolio_scheduler.set_schedule_times(["09:30"])
portfolio_scheduler.update_config(
analysis_mode="sequential",
max_workers=1,
auto_sync_monitor=True,
send_notification=True
)
st.success("已恢复默认配置!")
time.sleep(0.5)
st.rerun()
st.markdown("---")
# 控制按钮
col_btn1, col_btn2, col_btn3 = st.columns(3)
with col_btn1:
if is_running:
if st.button("⏹️ 停止调度器", type="secondary", use_container_width=True):
portfolio_scheduler.stop_scheduler()
st.success("调度器已停止")
time.sleep(0.5)
st.rerun()
else:
if st.button("▶️ 启动调度器", type="primary", use_container_width=True):
portfolio_scheduler.start_scheduler()
st.success("调度器已启动")
time.sleep(0.5)
st.rerun()
with col_btn2:
if st.button("🚀 立即执行一次", type="primary", use_container_width=True):
with st.spinner("正在执行持仓分析..."):
try:
portfolio_scheduler.run_analysis_now()
st.success("执行完成!请查看分析历史。")
except Exception as e:
st.error(f"执行失败: {str(e)}")
with col_btn3:
if st.button("🔄 刷新状态", use_container_width=True):
st.rerun()
def display_analysis_history():
"""显示分析历史"""
st.markdown("### 📈 分析历史记录")
stocks = portfolio_manager.get_all_stocks()
if not stocks:
st.info("暂无持仓股票")
return
# 选择股票
stock_codes = [s["code"] for s in stocks]
selected_code = st.selectbox(
"选择股票",
options=["全部"] + stock_codes,
help="查看特定股票的分析历史"
)
# 获取历史记录
if selected_code == "全部":
# 获取所有股票的最新历史
all_history = []
for stock in stocks:
stock_id = stock["id"]
history = portfolio_manager.db.get_latest_analysis_history(stock_id, limit=5)
for h in history:
h["code"] = stock["code"]
h["name"] = stock["name"]
all_history.extend(history)
# 按时间排序
all_history.sort(key=lambda x: x.get("analysis_time", ""), reverse=True)
history_list = all_history[:20] # 只显示最近20条
else:
# 获取指定股票的历史
stock = next((s for s in stocks if s["code"] == selected_code), None)
if stock:
history_list = portfolio_manager.db.get_latest_analysis_history(
stock["id"], limit=20
)
for h in history_list:
h["code"] = stock["code"]
h["name"] = stock["name"]
else:
history_list = []
if not history_list:
st.info(f"暂无分析历史记录")
return
# 显示历史记录
st.markdown(f"{len(history_list)} 条记录")
for record in history_list:
display_history_record(record)
def display_history_record(record: Dict):
"""显示单条历史记录"""
code = record.get("code", "")
name = record.get("name", "")
analysis_time = record.get("analysis_time", "")
rating = record.get("rating", "未知")
confidence = record.get("confidence", 0)
current_price = record.get("current_price")
target_price = record.get("target_price")
entry_min = record.get("entry_min")
entry_max = record.get("entry_max")
take_profit = record.get("take_profit")
stop_loss = record.get("stop_loss")
summary = record.get("summary", "")
# 评级颜色
if "强烈买入" in rating or "买入" in rating:
rating_icon = "🟢"
elif "卖出" in rating:
rating_icon = "🔴"
else:
rating_icon = "🟡"
with st.expander(
f"{rating_icon} {code} {name} - {rating} | {analysis_time}",
expanded=False
):
col1, col2, col3 = st.columns(3)
with col1:
st.markdown("**价格信息**")
if current_price:
st.write(f"当时价格: ¥{current_price:.2f}")
if target_price:
st.write(f"目标价: ¥{target_price:.2f}")
with col2:
st.markdown("**进场区间**")
if entry_min and entry_max:
st.write(f"¥{entry_min:.2f} ~ ¥{entry_max:.2f}")
with col3:
st.markdown("**风控位置**")
if take_profit:
st.write(f"止盈: ¥{take_profit:.2f}")
if stop_loss:
st.write(f"止损: ¥{stop_loss:.2f}")
if summary:
st.markdown("**分析摘要**")
st.info(summary)
st.caption(f"置信度: {confidence}%")