294 lines
10 KiB
Python
294 lines
10 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
低价擒牛策略监控UI模块
|
||
"""
|
||
|
||
import streamlit as st
|
||
import pandas as pd
|
||
from datetime import datetime
|
||
from low_price_bull_monitor import low_price_bull_monitor
|
||
from low_price_bull_service import low_price_bull_service
|
||
|
||
|
||
def display_monitor_panel():
|
||
"""显示策略监控面板"""
|
||
|
||
st.markdown("## 📊 策略监控中心")
|
||
st.markdown("---")
|
||
|
||
# 服务状态
|
||
status = low_price_bull_service.get_status()
|
||
|
||
col1, col2, col3, col4 = st.columns(4)
|
||
|
||
with col1:
|
||
service_status = "🟢 运行中" if status['running'] else "🔴 已停止"
|
||
st.metric("服务状态", service_status)
|
||
|
||
with col2:
|
||
st.metric("监控股票", f"{status['monitored_count']} 只")
|
||
|
||
with col3:
|
||
st.metric("待处理提醒", f"{status['pending_alerts']} 条")
|
||
|
||
with col4:
|
||
st.metric("扫描间隔", f"{status['scan_interval']} 秒")
|
||
|
||
st.markdown("---")
|
||
|
||
# 服务控制
|
||
col_start, col_stop, col_config = st.columns(3)
|
||
|
||
with col_start:
|
||
if st.button("▶️ 启动监控服务", type="primary", disabled=status['running']):
|
||
if low_price_bull_service.start():
|
||
st.success("✅ 监控服务已启动")
|
||
st.rerun()
|
||
else:
|
||
st.error("❌ 启动失败")
|
||
|
||
with col_stop:
|
||
if st.button("⏸️ 停止监控服务", type="secondary", disabled=not status['running']):
|
||
if low_price_bull_service.stop():
|
||
st.success("✅ 监控服务已停止")
|
||
st.rerun()
|
||
else:
|
||
st.error("❌ 停止失败")
|
||
|
||
with col_config:
|
||
with st.popover("⚙️ 监控配置"):
|
||
st.markdown("**扫描间隔**")
|
||
new_interval = st.number_input(
|
||
"扫描间隔(秒)",
|
||
min_value=10,
|
||
max_value=600,
|
||
value=status['scan_interval'],
|
||
step=10,
|
||
label_visibility="collapsed"
|
||
)
|
||
|
||
if st.button("保存配置"):
|
||
low_price_bull_service.scan_interval = new_interval
|
||
st.success("✅ 配置已保存")
|
||
|
||
st.markdown("---")
|
||
|
||
# 标签页
|
||
tab1, tab2, tab3 = st.tabs(["📋 监控列表", "🔔 卖出提醒", "📜 历史记录"])
|
||
|
||
with tab1:
|
||
display_monitored_stocks()
|
||
|
||
with tab2:
|
||
display_sell_alerts()
|
||
|
||
with tab3:
|
||
display_alert_history()
|
||
|
||
|
||
def display_monitored_stocks():
|
||
"""显示监控中的股票列表"""
|
||
|
||
stocks = low_price_bull_monitor.get_monitored_stocks()
|
||
|
||
if not stocks:
|
||
st.info("暂无监控中的股票")
|
||
return
|
||
|
||
st.markdown(f"### 📋 监控列表(共{len(stocks)}只)")
|
||
|
||
# 转换为DataFrame
|
||
df = pd.DataFrame(stocks)
|
||
|
||
# 显示表格
|
||
display_df = df[['stock_code', 'stock_name', 'buy_price', 'buy_date', 'holding_days', 'add_time']].copy()
|
||
display_df.columns = ['股票代码', '股票名称', '买入价格', '买入日期', '持有天数', '加入时间']
|
||
|
||
st.dataframe(display_df, width='content', height=400)
|
||
|
||
# 批量移除
|
||
st.markdown("---")
|
||
st.markdown("### 🗑️ 批量管理")
|
||
|
||
selected_codes = st.multiselect(
|
||
"选择要移除的股票",
|
||
options=[f"{s['stock_code']} {s['stock_name']}" for s in stocks],
|
||
format_func=lambda x: x
|
||
)
|
||
|
||
if selected_codes and st.button("🗑️ 移除选中股票", type="secondary"):
|
||
for item in selected_codes:
|
||
code = item.split()[0]
|
||
success, msg = low_price_bull_monitor.remove_stock(code, "手动移除")
|
||
if success:
|
||
st.success(msg)
|
||
else:
|
||
st.error(msg)
|
||
st.rerun()
|
||
|
||
|
||
def display_sell_alerts():
|
||
"""显示待处理的卖出提醒"""
|
||
|
||
alerts = low_price_bull_monitor.get_pending_alerts()
|
||
|
||
if not alerts:
|
||
st.info("暂无待处理的卖出提醒")
|
||
return
|
||
|
||
st.markdown(f"### 🔔 卖出提醒(共{len(alerts)}条)")
|
||
|
||
for alert in alerts:
|
||
with st.expander(
|
||
f"🔔 {alert['stock_code']} {alert['stock_name']} - {alert['alert_reason']}",
|
||
expanded=True
|
||
):
|
||
col1, col2 = st.columns(2)
|
||
|
||
with col1:
|
||
st.markdown("#### 📊 提醒信息")
|
||
st.markdown(f"**股票代码**: {alert['stock_code']}")
|
||
st.markdown(f"**股票名称**: {alert['stock_name']}")
|
||
st.markdown(f"**提醒类型**: {_get_alert_type_name(alert['alert_type'])}")
|
||
st.markdown(f"**提醒原因**: {alert['alert_reason']}")
|
||
st.markdown(f"**提醒时间**: {alert['alert_time']}")
|
||
|
||
with col2:
|
||
st.markdown("#### 💰 市场数据")
|
||
|
||
# 处理当前价格(避免bytes类型错误)
|
||
current_price = alert.get('current_price')
|
||
if current_price is not None:
|
||
try:
|
||
price_val = float(current_price)
|
||
st.markdown(f"**当前价格**: {price_val:.2f}元")
|
||
except (ValueError, TypeError):
|
||
st.markdown(f"**当前价格**: {current_price}")
|
||
|
||
# 处理MA5和MA20(避免bytes类型错误)
|
||
ma5 = alert.get('ma5')
|
||
ma20 = alert.get('ma20')
|
||
if ma5 is not None and ma20 is not None:
|
||
try:
|
||
ma5_val = float(ma5)
|
||
ma20_val = float(ma20)
|
||
st.markdown(f"**MA5**: {ma5_val:.2f}")
|
||
st.markdown(f"**MA20**: {ma20_val:.2f}")
|
||
st.markdown(f"**均线差**: {(ma5_val - ma20_val):.2f}")
|
||
except (ValueError, TypeError):
|
||
st.markdown(f"**MA5**: {ma5}")
|
||
st.markdown(f"**MA20**: {ma20}")
|
||
|
||
# 处理持有天数
|
||
holding_days = alert.get('holding_days')
|
||
if holding_days is not None:
|
||
try:
|
||
days_val = int(holding_days)
|
||
st.markdown(f"**持有天数**: {days_val}天")
|
||
except (ValueError, TypeError):
|
||
st.markdown(f"**持有天数**: {holding_days}")
|
||
|
||
# 操作按钮
|
||
st.markdown("---")
|
||
col_action1, col_action2 = st.columns(2)
|
||
|
||
with col_action1:
|
||
if st.button(f"✅ 已处理 - {alert['stock_code']}", key=f"done_{alert['id']}"):
|
||
low_price_bull_monitor.mark_alert_sent(alert['id'])
|
||
low_price_bull_monitor.remove_stock(alert['stock_code'], "已处理提醒")
|
||
st.success(f"✅ 已标记为已处理并移除 {alert['stock_code']}")
|
||
st.rerun()
|
||
|
||
with col_action2:
|
||
if st.button(f"❌ 忽略 - {alert['stock_code']}", key=f"ignore_{alert['id']}"):
|
||
low_price_bull_monitor.mark_alert_sent(alert['id'])
|
||
st.success(f"✅ 已忽略提醒(股票保留在监控列表)")
|
||
st.rerun()
|
||
|
||
|
||
def display_alert_history():
|
||
"""显示历史提醒记录"""
|
||
|
||
alerts = low_price_bull_monitor.get_history_alerts(limit=50)
|
||
|
||
if not alerts:
|
||
st.info("暂无历史提醒记录")
|
||
return
|
||
|
||
st.markdown("### 📜 历史提醒记录")
|
||
|
||
# 转换为DataFrame
|
||
df = pd.DataFrame(alerts)
|
||
|
||
# 选择显示列
|
||
display_cols = ['stock_code', 'stock_name', 'alert_type', 'alert_reason', 'current_price', 'holding_days', 'alert_time', 'is_sent']
|
||
display_df = df[display_cols].copy()
|
||
|
||
# 重命名列
|
||
display_df.columns = ['股票代码', '股票名称', '提醒类型', '提醒原因', '当前价格', '持有天数', '提醒时间', '已发送']
|
||
|
||
# 格式化
|
||
display_df['提醒类型'] = display_df['提醒类型'].apply(_get_alert_type_name)
|
||
display_df['已发送'] = display_df['已发送'].apply(lambda x: '✅' if x == 1 else '❌')
|
||
|
||
st.dataframe(display_df, width='content', height=400)
|
||
|
||
# 清理按钮
|
||
st.markdown("---")
|
||
if st.button("🗑️ 清理30天前的记录"):
|
||
low_price_bull_monitor.clear_old_alerts(days=30)
|
||
st.success("✅ 已清理旧记录")
|
||
st.rerun()
|
||
|
||
|
||
def _get_alert_type_name(alert_type: str) -> str:
|
||
"""获取提醒类型名称"""
|
||
type_map = {
|
||
'holding_days': '持股到期',
|
||
'ma_cross': 'MA均线死叉'
|
||
}
|
||
return type_map.get(alert_type, alert_type)
|
||
|
||
|
||
def add_stock_to_monitor_button(stock_code: str, stock_name: str, price: float = None):
|
||
"""
|
||
添加股票到监控按钮(在股票详情中使用)
|
||
|
||
Args:
|
||
stock_code: 股票代码
|
||
stock_name: 股票名称
|
||
price: 买入价格
|
||
"""
|
||
# 检查是否已在监控
|
||
stocks = low_price_bull_monitor.get_monitored_stocks()
|
||
is_monitored = any(s['stock_code'] == stock_code for s in stocks)
|
||
|
||
if is_monitored:
|
||
st.info(f"✅ {stock_code} 已在监控列表中")
|
||
if st.button(f"🗑️ 移出监控 - {stock_code}", key=f"remove_{stock_code}"):
|
||
success, msg = low_price_bull_monitor.remove_stock(stock_code, "手动移除")
|
||
if success:
|
||
st.success(msg)
|
||
st.rerun()
|
||
else:
|
||
st.error(msg)
|
||
else:
|
||
if st.button(f"➕ 加入策略监控 - {stock_code}", type="primary", key=f"add_{stock_code}"):
|
||
if price is None:
|
||
price = 0.0 # 如果没有价格,使用0
|
||
|
||
success, msg = low_price_bull_monitor.add_stock(
|
||
stock_code=stock_code,
|
||
stock_name=stock_name,
|
||
buy_price=price,
|
||
buy_date=datetime.now().strftime("%Y-%m-%d")
|
||
)
|
||
|
||
if success:
|
||
st.success(msg)
|
||
st.info("💡 提示:请在左侧菜单进入'策略监控'查看监控状态")
|
||
st.rerun()
|
||
else:
|
||
st.error(msg)
|