Files
aiagents-stock/profit_growth_ui.py
T
2025-12-14 12:31:01 +08:00

426 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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
from profit_growth_selector import profit_growth_selector
from notification_service import notification_service
from profit_growth_monitor import profit_growth_monitor
def display_profit_growth():
"""显示净利增长策略界面"""
# 检查是否显示监控面板
if st.session_state.get('show_profit_growth_monitor'):
display_profit_growth_monitor_panel()
# 返回按钮
if st.button("🔙 返回选股", type="secondary"):
del st.session_state.show_profit_growth_monitor
st.rerun()
return
st.markdown("### 顶部按钮区")
col_select, col_monitor = st.columns([3, 1])
with col_select:
st.markdown("## 📈 净利增长策略 - 稳健成长股票筛选")
with col_monitor:
st.write("") # 占位
if st.button("📊 策略监控", type="primary", use_container_width=True):
st.session_state.show_profit_growth_monitor = True
st.rerun()
st.markdown("---")
st.markdown("""
### 📋 选股策略说明
**筛选条件**
- ✅ 净利润增长率 ≥ 10%(净利润同比增长率)
- ✅ 深圳A股
- ✅ 非ST股票
- ✅ 非创业板
- ✅ 非科创板
- ✅ 按成交额由小到大排名
**量化交易策略**
- 💰 资金量:5万元
- 📅 持股周期:5天
- 💼 仓位控制:满仓
- 📊 个股最大持仓:4成(40%)
- 🎯 账户最大持股数:4只
- 🛒 单日最大买入数:1只
- 📈 买入时机:开盘买入
- 📉 卖出时机:KDJ死叉或持股满5天
> **注意**:当前监控服务暂时使用MA5下穿MA20作为卖出信号,后续将升级支持KDJ指标。
""")
st.markdown("---")
# 参数设置
col1, col2 = st.columns([2, 1])
with col1:
top_n = st.slider(
"筛选数量",
min_value=3,
max_value=10,
value=5,
step=1,
help="选择展示的股票数量"
)
with col2:
st.info(f"💡 将筛选成交额最小的前{top_n}只股票")
st.markdown("---")
# 开始选股按钮
if st.button("🚀 开始净利增长选股", type="primary", use_container_width=True):
with st.spinner("正在获取数据,请稍候..."):
# 创建选股器
success, stocks_df, message = profit_growth_selector.get_profit_growth_stocks(top_n)
if not success:
st.error(f"❌ {message}")
return
st.success(f"✅ {message}")
# 保存到session_state
st.session_state.profit_growth_stocks = stocks_df
st.session_state.profit_growth_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# 显示选股结果
if 'profit_growth_stocks' in st.session_state and st.session_state.profit_growth_stocks is not None:
st.markdown("---")
st.markdown("## 📈 选股结果")
stocks_df = st.session_state.profit_growth_stocks
select_time = st.session_state.profit_growth_time
st.info(f"🕒 选股时间:{select_time} | 📊 股票数量:{len(stocks_df)} 只")
# 显示股票列表
display_stock_list(stocks_df)
# 发送钉钉通知
st.markdown("---")
if st.button("📲 发送钉钉通知", type="secondary", use_container_width=True):
send_dingtalk_notification(stocks_df)
def display_stock_list(stocks_df: pd.DataFrame):
"""显示股票列表"""
for idx, row in stocks_df.iterrows():
stock_code = row.get('股票代码', 'N/A')
stock_name = row.get('股票简称', 'N/A')
with st.expander(f"📊 {idx+1}. {stock_code} {stock_name}", expanded=True):
display_stock_detail(row)
def display_stock_detail(row: pd.Series):
"""显示股票详细信息"""
# 获取所有可能的字段
financial_fields = [
('净利润增长率', row.get('净利润增长率', row.get('净利润同比增长率', None))),
('成交额', row.get('成交额', row.get('成交额[20241213]', None))),
('股价', row.get('股价', row.get('最新价', None))),
('市盈率', row.get('市盈率', row.get('市盈率TTM', None))),
('市净率', row.get('市净率', row.get('市净率PB', None))),
('所属行业', row.get('所属行业', row.get('所属同花顺行业', None))),
]
# 检查是否有任何有效数据
has_any_data = any(is_valid_value(value) for _, value in financial_fields)
# 决定布局
if has_any_data:
col1, col2 = st.columns(2)
else:
col1 = st.container()
col2 = None
with col1:
st.markdown("#### 📊 基本信息")
st.markdown(f"**股票代码**: {row.get('股票代码', 'N/A')}")
st.markdown(f"**股票名称**: {row.get('股票简称', 'N/A')}")
# 只有当有财务数据时才显示财务指标
if col2 is not None:
with col2:
st.markdown("#### 💼 财务指标")
for field_name, value in financial_fields:
if is_valid_value(value):
formatted_value = format_value(value, get_suffix(field_name))
st.markdown(f"**{field_name}**: {formatted_value}")
# 添加监控按钮
st.markdown("---")
st.markdown("#### 📊 策略监控")
stock_code = row.get('股票代码', '')
stock_name = row.get('股票简称', '')
price = row.get('股价', row.get('最新价', None))
# 去掉代码后缀
if isinstance(stock_code, str) and '.' in stock_code:
stock_code = stock_code.split('.')[0]
# 转换价格
try:
price_float = float(price) if price and not pd.isna(price) else None
except:
price_float = None
if stock_code and stock_name:
add_stock_to_monitor_button(stock_code, stock_name, price_float)
def add_stock_to_monitor_button(stock_code: str, stock_name: str, price: float = None):
"""添加股票到监控的按钮"""
button_key = f"add_monitor_{stock_code}"
if st.button(f" 加入策略监控", key=button_key, use_container_width=True):
# 获取价格
if price is None:
st.warning("⚠️ 无法获取股票价格,请手动输入")
return
# 添加到监控
success, message = profit_growth_monitor.add_stock(
stock_code=stock_code,
stock_name=stock_name,
buy_price=price
)
if success:
st.success(f"✅ {message}")
else:
st.error(f"❌ {message}")
def display_profit_growth_monitor_panel():
"""显示净利增长监控面板"""
st.markdown("## 📊 净利增长策略监控")
st.markdown("---")
# 获取监控中的股票
monitoring_stocks = profit_growth_monitor.get_monitoring_stocks()
# 标签页
tab1, tab2, tab3 = st.tabs(["📊 监控列表", "⚠️ 卖出提醒", "📜 历史记录"])
with tab1:
display_monitoring_list(monitoring_stocks)
with tab2:
display_sell_alerts()
with tab3:
display_history()
def display_monitoring_list(stocks: List[Dict]):
"""显示监控列表"""
st.markdown("### 📊 持仓监控")
if not stocks:
st.info("📭 监控列表为空,请先添加股票")
return
st.info(f"📊 当前监控 {len(stocks)} 只股票")
for stock in stocks:
with st.expander(f"📊 {stock['stock_code']} {stock['stock_name']}", expanded=False):
col1, col2, col3 = st.columns([2, 2, 1])
with col1:
st.markdown(f"**买入价格**: {stock['buy_price']:.2f}元")
st.markdown(f"**买入日期**: {stock['buy_date']}")
with col2:
st.markdown(f"**持股天数**: {stock['holding_days']}天")
st.markdown(f"**加入时间**: {stock['add_time']}")
with col3:
if st.button("🗑️ 移除", key=f"remove_{stock['stock_code']}", use_container_width=True):
success, msg = profit_growth_monitor.remove_stock(stock['stock_code'], "手动移除")
if success:
st.success(msg)
st.rerun()
else:
st.error(msg)
def display_sell_alerts():
"""显示卖出提醒"""
st.markdown("### ⚠️ 卖出提醒")
alerts = profit_growth_monitor.get_unprocessed_alerts()
if not alerts:
st.info("📭 暂无新的卖出提醒")
return
st.warning(f"⚠️ 有 {len(alerts)} 条待处理提醒")
for alert in alerts:
with st.container():
st.markdown("---")
col1, col2 = st.columns([3, 1])
with col1:
st.markdown(f"### 📊 {alert['stock_code']} {alert['stock_name']}")
st.markdown(f"**提醒类型**: {alert['alert_type']}")
st.markdown(f"**提醒原因**: {alert['alert_reason']}")
st.markdown(f"**提醒时间**: {alert['alert_time']}")
with col2:
st.markdown("#### 💰 市场数据")
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}")
holding_days = alert.get('holding_days')
if holding_days is not None:
st.markdown(f"**持有天数**: {holding_days}天")
def display_history():
"""显示历史记录"""
st.markdown("### 📜 历史记录")
# 子标签
sub_tab1, sub_tab2 = st.tabs(["⚠️ 提醒历史", "🗑️ 移除历史"])
with sub_tab1:
alerts = profit_growth_monitor.get_all_alerts(50)
if alerts:
st.info(f"📊 共 {len(alerts)} 条提醒记录")
for alert in alerts:
st.markdown(f"- **{alert['alert_time']}** | {alert['stock_code']} {alert['stock_name']} | {alert['alert_type']}")
else:
st.info("📭 暂无提醒历史")
with sub_tab2:
removed = profit_growth_monitor.get_removed_stocks(50)
if removed:
st.info(f"📊 共 {len(removed)} 条移除记录")
for stock in removed:
st.markdown(f"- **{stock['remove_time']}** | {stock['stock_code']} {stock['stock_name']} | {stock['remove_reason']}")
else:
st.info("📭 暂无移除历史")
def is_valid_value(value):
"""判断值是否有效"""
if value is None:
return False
if pd.isna(value):
return False
if str(value).strip() in ['', 'N/A', 'nan', 'None']:
return False
return True
def format_value(value, suffix=''):
"""格式化显示值"""
if isinstance(value, (int, float)):
if abs(value) >= 100000000: # 亿
return f"{value/100000000:.2f}亿{suffix}"
elif abs(value) >= 10000: # 万
return f"{value/10000:.2f}{suffix}"
else:
return f"{value:.2f}{suffix}"
return f"{value}{suffix}"
def get_suffix(field_name: str) -> str:
"""获取字段后缀"""
suffix_map = {
'净利润增长率': '%',
'成交额': '元',
'股价': '元',
}
return suffix_map.get(field_name, '')
def send_dingtalk_notification(stocks_df: pd.DataFrame):
"""发送钉钉通知"""
try:
if not notification_service.config['webhook_enabled']:
st.warning("⚠️ Webhook通知未启用,请在系统配置中启用")
return
# 构建消息
keyword = notification_service.config.get('webhook_keyword', 'aiagents通知')
message_text = f"### {keyword} - 净利增长选股完成\n\n"
message_text += "**筛选策略**: 净利润增长率≥10% + 深圳A股 + 非ST/创业/科创\n\n"
message_text += f"**筛选数量**: {len(stocks_df)}\n\n"
message_text += "**精选股票**:\n\n"
for idx, row in stocks_df.iterrows():
stock_code = row.get('股票代码', 'N/A')
stock_name = row.get('股票简称', 'N/A')
message_text += f"{idx+1}. {stock_code} {stock_name}\n\n"
message_text += f"**生成时间**: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n\n"
message_text += "_此消息由AI股票分析系统自动发送_"
# 直接发送钉钉Webhook
if notification_service.config['webhook_type'] == 'dingtalk':
import requests
data = {
"msgtype": "markdown",
"markdown": {
"title": f"{keyword}",
"text": message_text
}
}
response = requests.post(
notification_service.config['webhook_url'],
json=data,
headers={'Content-Type': 'application/json'},
timeout=10
)
if response.status_code == 200:
st.success("✅ 钉钉通知发送成功")
else:
st.error(f"❌ 钉钉通知发送失败: HTTP {response.status_code}")
else:
st.warning("⚠️ 当前仅支持钉钉通知")
except Exception as e:
st.error(f"❌ 发送通知失败: {str(e)}")