Files
aiagents-stock/value_stock_ui.py
T
oficcejo 9ee27de0ed update
2026-02-27 20:06:45 +08:00

421 lines
13 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 value_stock_selector import ValueStockSelector
from value_stock_strategy import ValueStockStrategy
def display_value_stock():
"""显示低估值选股界面"""
st.markdown("""
<div style="background: linear-gradient(135deg, #1a5276 0%, #2e86c1 50%, #1a5276 100%);
padding: 2rem; border-radius: 15px; margin-bottom: 1.5rem;
box-shadow: 0 8px 32px rgba(0,0,0,0.3);">
<h1 style="color: #fff; margin: 0; font-size: 2rem;">
💎 低估值策略 - 价值投资选股
</h1>
<p style="color: rgba(255,255,255,0.7); margin: 0.5rem 0 0 0; font-size: 0.9rem;">
基于视频 <a href="https://www.bilibili.com/video/BV1eJfxBrEjZ" target="_blank" style="color: #7ec8e3; text-decoration: underline;">头号投资法则</a>
</p>
<p style="color: rgba(255,255,255,0.8); margin: 0.3rem 0 0 0; font-size: 1.1rem;">
低PE + 低PB + 高股息 + 低负债 — 寻找被市场低估的优质标的
</p>
</div>
""", unsafe_allow_html=True)
st.markdown("---")
st.markdown("""
### 📋 选股策略说明
**筛选条件**
- ✅ 市盈率(PE)≤ 20
- ✅ 市净率(PB)≤ 1.5
- ✅ 股息率 ≥ 1%
- ✅ 资产负债率 ≤ 30%
- ✅ 非ST股票
- ✅ 非科创板
- ✅ 非创业板
- ✅ 按流通市值由小到大排名
**量化交易策略**
- 💰 资金量:100万元
- 📈 买入时机:开盘买入
- 💼 单股最大仓位:30%
- 🎯 最大持股数:4只
- 🛒 每日最多买入:2只
- 📉 卖出条件①:持股满30天到期卖出
- 📉 卖出条件②:RSI超买(>70)卖出
""")
st.markdown("---")
# 参数设置
col1, col2 = st.columns([2, 1])
with col1:
top_n = st.slider(
"筛选数量",
min_value=5,
max_value=20,
value=10,
step=1,
help="选择展示的股票数量",
key="value_stock_top_n"
)
with col2:
st.info(f"💡 将筛选流通市值最小的前{top_n}只低估值股票")
st.markdown("---")
# 开始选股按钮
if st.button("🚀 开始低估值选股", type="primary", width='content', key="value_stock_start"):
with st.spinner("正在获取数据,请稍候..."):
selector = ValueStockSelector()
success, stocks_df, message = selector.get_value_stocks(top_n=top_n)
if success and stocks_df is not None:
st.session_state.value_stocks = stocks_df
st.session_state.value_stock_selector = selector
st.success(f"✅ {message}")
st.rerun()
else:
st.error(f"❌ {message}")
# 显示选股结果
if 'value_stocks' in st.session_state:
display_stock_results(
st.session_state.value_stocks,
st.session_state.get('value_stock_selector')
)
def display_stock_results(stocks_df: pd.DataFrame, selector):
"""显示选股结果"""
st.markdown("---")
st.markdown("## 📊 选股结果")
# 统计信息
col1, col2, col3, col4 = st.columns(4)
with col1:
st.metric("筛选数量", f"{len(stocks_df)} 只")
with col2:
pe_col = None
for pattern in ['市盈率', '市盈率(动态)']:
matching = [col for col in stocks_df.columns if pattern in col]
if matching:
pe_col = matching[0]
break
if pe_col:
valid = pd.to_numeric(stocks_df[pe_col], errors='coerce').dropna()
if len(valid) > 0:
st.metric("平均PE", f"{valid.mean():.1f}")
else:
st.metric("平均PE", "-")
else:
st.metric("平均PE", "-")
with col3:
pb_col = None
matching = [col for col in stocks_df.columns if '市净率' in col]
if matching:
pb_col = matching[0]
valid = pd.to_numeric(stocks_df[pb_col], errors='coerce').dropna()
if len(valid) > 0:
st.metric("平均PB", f"{valid.mean():.2f}")
else:
st.metric("平均PB", "-")
else:
st.metric("平均PB", "-")
with col4:
div_col = None
matching = [col for col in stocks_df.columns if '股息率' in col]
if matching:
div_col = matching[0]
valid = pd.to_numeric(stocks_df[div_col], errors='coerce').dropna()
if len(valid) > 0:
st.metric("平均股息率", f"{valid.mean():.2f}%")
else:
st.metric("平均股息率", "-")
else:
st.metric("平均股息率", "-")
st.markdown("---")
# 显示股票列表
st.markdown("### 📋 精选低估值股票")
for idx, row in stocks_df.iterrows():
code = row.get('股票代码', 'N/A')
name = row.get('股票简称', 'N/A')
# 获取关键指标用于标题
pe_val = ''
for pattern in ['市盈率', '市盈率(动态)']:
matching = [col for col in stocks_df.columns if pattern in col]
if matching:
v = row.get(matching[0])
if v is not None and not pd.isna(v):
try:
pe_val = f" PE:{float(v):.1f}"
except:
pass
break
pb_val = ''
matching = [col for col in stocks_df.columns if '市净率' in col]
if matching:
v = row.get(matching[0])
if v is not None and not pd.isna(v):
try:
pb_val = f" PB:{float(v):.2f}"
except:
pass
with st.expander(
f"【第{idx+1}名】{code} - {name}{pe_val}{pb_val}",
expanded=(idx < 3)
):
display_stock_detail(row, stocks_df)
# 完整数据表格
st.markdown("---")
st.markdown("### 📊 完整数据表格")
# 选择关键列
display_cols = ['股票代码', '股票简称']
for pattern in ['最新价', '股价']:
matching = [col for col in stocks_df.columns if pattern in col]
if matching:
display_cols.append(matching[0])
break
for pattern in ['市盈率', '市净率', '股息率', '资产负债率', '流通市值', '所属行业']:
matching = [col for col in stocks_df.columns if pattern in col]
if matching:
display_cols.append(matching[0])
final_cols = [col for col in display_cols if col in stocks_df.columns]
if final_cols:
st.dataframe(stocks_df[final_cols], width='content', height=400)
csv = stocks_df[final_cols].to_csv(index=False, encoding='utf-8-sig')
st.download_button(
label="📥 下载股票列表CSV",
data=csv,
file_name=f"value_stock_{datetime.now().strftime('%Y%m%d')}.csv",
mime="text/csv",
key="value_csv_download"
)
# 量化交易模拟
st.markdown("---")
display_strategy_simulation(stocks_df, selector)
def display_stock_detail(row: pd.Series, df: pd.DataFrame):
"""显示单个股票详情"""
def is_valid(value):
if value is None:
return False
if isinstance(value, float) and pd.isna(value):
return False
if isinstance(value, str) and value.strip() in ('', 'N/A', 'nan', 'None'):
return False
return True
def fmt(value, suffix=''):
if not is_valid(value):
return "-"
try:
return f"{float(value):.2f}{suffix}"
except:
return str(value) + suffix
# 基本估值数据
col1, col2, col3, col4 = st.columns(4)
with col1:
for p in ['市盈率', '市盈率(动态)']:
m = [c for c in df.columns if p in c]
if m:
st.metric("📊 市盈率(PE)", fmt(row.get(m[0])))
break
with col2:
m = [c for c in df.columns if '市净率' in c]
if m:
st.metric("📊 市净率(PB)", fmt(row.get(m[0])))
with col3:
m = [c for c in df.columns if '股息率' in c]
if m:
st.metric("💰 股息率", fmt(row.get(m[0]), '%'))
with col4:
m = [c for c in df.columns if '资产负债率' in c]
if m:
st.metric("📉 资产负债率", fmt(row.get(m[0]), '%'))
# 补充信息
st.markdown("**其他指标**")
info_parts = []
for pattern in ['最新价', '股价', '流通市值', '总市值', '所属行业', '涨跌幅']:
m = [c for c in df.columns if pattern in c]
if m:
val = row.get(m[0])
if is_valid(val):
info_parts.append(f"**{pattern}**: {val}")
if info_parts:
st.markdown(" | ".join(info_parts))
def display_strategy_simulation(stocks_df: pd.DataFrame, selector):
"""显示量化交易策略模拟"""
st.markdown("## 🎯 策略模拟")
st.info("""
**策略规则**
- 📈 **买入**:开盘价买入,单股最大仓位30%,每日最多买2只
- 📉 **卖出条件①**:持股满30天,到期自动卖出
- 📉 **卖出条件②**RSI(14) > 70 超买,触发卖出
- 🎯 **最大持股**:4只
- 💰 **初始资金**100万元
""")
col1, col2 = st.columns(2)
with col1:
if st.button("🎮 开始策略模拟", type="primary", width='content', key="value_sim_start"):
st.session_state.show_value_strategy_sim = True
with col2:
pass
if st.session_state.get('show_value_strategy_sim'):
run_strategy_simulation(stocks_df)
def run_strategy_simulation(stocks_df: pd.DataFrame):
"""运行策略模拟"""
st.markdown("---")
st.markdown("### 📈 策略模拟执行")
strategy = ValueStockStrategy(initial_capital=1000000.0)
# 模拟买入
st.markdown("#### 1️⃣ 模拟买入信号")
buy_results = []
current_date = datetime.now().strftime("%Y-%m-%d")
for idx, row in stocks_df.head(strategy.max_daily_buy).iterrows():
code = str(row.get('股票代码', '')).split('.')[0]
name = row.get('股票简称', 'N/A')
# 尝试获取价格
price = 0
for p in ['最新价', '股价']:
m = [c for c in stocks_df.columns if p in c]
if m:
try:
price = float(row.get(m[0], 0))
except:
pass
if price > 0:
break
if price > 0:
success, message, trade = strategy.buy(code, name, price, current_date)
buy_results.append({
'success': success,
'message': message,
'trade': trade
})
for result in buy_results:
if result['success']:
st.success(result['message'])
else:
st.warning(f"⚠️ {result['message']}")
# RSI检查
st.markdown("---")
st.markdown("#### 2️⃣ RSI卖出信号检测")
with st.spinner("正在计算RSI指标..."):
for code, pos in list(strategy.positions.items()):
rsi = strategy.calculate_rsi(code)
if rsi is not None:
if rsi > strategy.rsi_overbought:
st.warning(f"⚠️ {code} {pos['name']} RSI={rsi} > {strategy.rsi_overbought},触发超买卖出信号!")
else:
st.info(f"{code} {pos['name']} RSI={rsi},正常范围")
else:
st.info(f"{code} {pos['name']} RSI计算中...")
# 显示持仓
st.markdown("---")
st.markdown("#### 3️⃣ 当前持仓")
positions = strategy.get_positions()
if positions:
positions_df = pd.DataFrame(positions)
st.dataframe(positions_df, width='content')
else:
st.info("暂无持仓")
# 显示账户摘要
st.markdown("---")
st.markdown("#### 4️⃣ 账户摘要")
summary = strategy.get_portfolio_summary()
col1, col2, col3, col4 = st.columns(4)
with col1:
st.metric("初始资金", f"{summary['initial_capital']:,.0f} 元")
with col2:
st.metric("可用资金", f"{summary['available_cash']:,.0f} 元")
with col3:
st.metric("持仓市值", f"{summary['position_value']:,.0f} 元")
with col4:
st.metric("总资产", f"{summary['total_assets']:,.0f} 元")
st.markdown("---")
st.markdown("#### 📝 策略说明")
st.markdown("""
**后续操作**
1. **持有期管理**:系统跟踪每只股票的持有天数(30天到期)
2. **RSI监测**:每日收盘后计算RSI(14)
- RSI > 70:超买信号,提示卖出
- RSI < 30:超卖信号(可作为加仓参考)
3. **轮动买入**:卖出后释放资金,继续买入新的低估值股票
**风险提示**
- ⚠️ 本策略为模拟演示,实际交易存在滑点、手续费等成本
- ⚠️ 低估值不代表没有风险,价值陷阱需警惕
- ⚠️ 请谨慎评估风险,理性投资
""")
# 主入口
if __name__ == "__main__":
display_value_stock()