增加选股策略

This commit is contained in:
oficcejo
2025-12-14 12:31:01 +08:00
parent 941a6cd541
commit be76e444c7
6 changed files with 899 additions and 2 deletions
+425
View File
@@ -0,0 +1,425 @@
#!/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)}")