feat(viz): 添加 Streamlit 可视化应用
- 创建 streamlit_app.py: 交互式回测结果展示 - 支持4个标签页:策略概览、收益分析、调仓记录、品种详情 - 集成 Plotly 图表:收益对比、胜率散点图、月度收益、收益分布 - 支持数据筛选和导出功能 - 添加启动脚本和依赖文件
This commit is contained in:
5
requirements_streamlit.txt
Normal file
5
requirements_streamlit.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
# Streamlit 可视化应用依赖
|
||||
streamlit>=1.28.0
|
||||
plotly>=5.15.0
|
||||
pandas>=2.0.0
|
||||
requests>=2.31.0
|
||||
24
start_streamlit.sh
Executable file
24
start_streamlit.sh
Executable file
@@ -0,0 +1,24 @@
|
||||
#!/bin/bash
|
||||
# Streamlit 应用启动脚本
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
echo "🚀 启动 ETF轮动策略可视化应用..."
|
||||
echo ""
|
||||
|
||||
# 检查虚拟环境
|
||||
if [ -d "venv" ]; then
|
||||
source venv/bin/activate
|
||||
fi
|
||||
|
||||
# 安装依赖(如果需要)
|
||||
# pip install -r requirements_streamlit.txt
|
||||
|
||||
# 设置环境变量
|
||||
export API_BASE_URL=${API_BASE_URL:-"https://k3s.tokenpluse.xyz"}
|
||||
|
||||
echo "📊 正在启动 Streamlit 服务..."
|
||||
echo "🌐 API地址: $API_BASE_URL"
|
||||
echo ""
|
||||
|
||||
streamlit run streamlit_app.py --server.port 8501 --server.address 0.0.0.0
|
||||
366
streamlit_app.py
Normal file
366
streamlit_app.py
Normal file
@@ -0,0 +1,366 @@
|
||||
"""
|
||||
ETF轮动策略回测结果可视化
|
||||
============================
|
||||
使用 Streamlit 展示策略回测结果
|
||||
|
||||
运行: streamlit run streamlit_app.py
|
||||
"""
|
||||
|
||||
import streamlit as st
|
||||
import pandas as pd
|
||||
import plotly.express as px
|
||||
import plotly.graph_objects as go
|
||||
from plotly.subplots import make_subplots
|
||||
import requests
|
||||
from datetime import datetime
|
||||
import os
|
||||
|
||||
# 页面配置
|
||||
st.set_page_config(
|
||||
page_title="ETF轮动策略回测结果",
|
||||
page_icon="📈",
|
||||
layout="wide",
|
||||
initial_sidebar_state="expanded"
|
||||
)
|
||||
|
||||
# 样式
|
||||
st.markdown("""
|
||||
<style>
|
||||
.main-header {
|
||||
font-size: 2.5rem;
|
||||
font-weight: bold;
|
||||
color: #1f77b4;
|
||||
}
|
||||
.metric-card {
|
||||
background-color: #f0f2f6;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.positive { color: #28a745; }
|
||||
.negative { color: #dc3545; }
|
||||
</style>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# 标题
|
||||
st.markdown('<p class="main-header">📈 ETF轮动策略回测结果</p>', unsafe_allow_html=True)
|
||||
st.markdown("---")
|
||||
|
||||
# 侧边栏配置
|
||||
st.sidebar.header("⚙️ 配置")
|
||||
|
||||
# 数据API配置
|
||||
api_base = st.sidebar.text_input(
|
||||
"数据API地址",
|
||||
value=os.getenv('API_BASE_URL', 'https://k3s.tokenpluse.xyz'),
|
||||
help="Flask API服务地址"
|
||||
)
|
||||
|
||||
# 加载数据
|
||||
@st.cache_data(ttl=3600)
|
||||
def load_summary_data():
|
||||
"""加载策略汇总数据"""
|
||||
try:
|
||||
df = pd.read_csv('results/report_summary.csv')
|
||||
# 转换百分比字符串为数值
|
||||
for col in ['胜率', '平均收益', '累计收益', '最大单次收益', '最大单次亏损']:
|
||||
if col in df.columns:
|
||||
df[col] = df[col].str.rstrip('%').astype(float)
|
||||
return df
|
||||
except Exception as e:
|
||||
st.error(f"加载汇总数据失败: {e}")
|
||||
return None
|
||||
|
||||
@st.cache_data(ttl=3600)
|
||||
def load_trades_data():
|
||||
"""加载交易记录数据"""
|
||||
try:
|
||||
df = pd.read_csv('results/report_trades.csv')
|
||||
df['进场日期'] = pd.to_datetime(df['进场日期'])
|
||||
df['出场日期'] = pd.to_datetime(df['出场日期'])
|
||||
return df
|
||||
except Exception as e:
|
||||
st.error(f"加载交易数据失败: {e}")
|
||||
return None
|
||||
|
||||
# 加载数据
|
||||
summary_df = load_summary_data()
|
||||
trades_df = load_trades_data()
|
||||
|
||||
# 主页面标签页
|
||||
tab1, tab2, tab3, tab4 = st.tabs(["📊 策略概览", "📈 收益分析", "🔄 调仓记录", "🔍 品种详情"])
|
||||
|
||||
# ============ Tab 1: 策略概览 ============
|
||||
with tab1:
|
||||
st.header("策略绩效概览")
|
||||
|
||||
if summary_df is not None:
|
||||
# 关键指标
|
||||
col1, col2, col3, col4 = st.columns(4)
|
||||
|
||||
with col1:
|
||||
total_trades = summary_df['调仓次数'].sum()
|
||||
st.metric("总调仓次数", f"{total_trades:,}")
|
||||
|
||||
with col2:
|
||||
avg_win_rate = summary_df['胜率'].mean()
|
||||
st.metric("平均胜率", f"{avg_win_rate:.2f}%")
|
||||
|
||||
with col3:
|
||||
best_return = summary_df['累计收益'].max()
|
||||
best_code = summary_df.loc[summary_df['累计收益'].idxmax(), '品种代码']
|
||||
st.metric("最佳品种收益", f"{best_return:.2f}%", f"{best_code}")
|
||||
|
||||
with col4:
|
||||
worst_return = summary_df['累计收益'].min()
|
||||
worst_code = summary_df.loc[summary_df['累计收益'].idxmin(), '品种代码']
|
||||
st.metric("最差品种收益", f"{worst_return:.2f}%", f"{worst_code}")
|
||||
|
||||
st.markdown("---")
|
||||
|
||||
# 品种收益对比
|
||||
fig = px.bar(
|
||||
summary_df.sort_values('累计收益', ascending=True),
|
||||
x='累计收益',
|
||||
y='品种名称',
|
||||
orientation='h',
|
||||
color='累计收益',
|
||||
color_continuous_scale=['red', 'yellow', 'green'],
|
||||
title="各品种累计收益对比",
|
||||
labels={'累计收益': '累计收益 (%)', '品种名称': ''}
|
||||
)
|
||||
fig.update_layout(height=400)
|
||||
st.plotly_chart(fig, use_container_width=True)
|
||||
|
||||
# 胜率 vs 平均收益散点图
|
||||
fig2 = px.scatter(
|
||||
summary_df,
|
||||
x='胜率',
|
||||
y='平均收益',
|
||||
size='调仓次数',
|
||||
color='品种名称',
|
||||
hover_data=['品种代码', '累计收益'],
|
||||
title="胜率 vs 平均收益(气泡大小=调仓次数)",
|
||||
labels={'胜率': '胜率 (%)', '平均收益': '平均收益 (%)'}
|
||||
)
|
||||
st.plotly_chart(fig2, use_container_width=True)
|
||||
|
||||
# 数据表格
|
||||
st.subheader("详细数据")
|
||||
st.dataframe(
|
||||
summary_df.style.format({
|
||||
'胜率': '{:.2f}%',
|
||||
'平均收益': '{:.2f}%',
|
||||
'累计收益': '{:.2f}%',
|
||||
'最大单次收益': '{:.2f}%',
|
||||
'最大单次亏损': '{:.2f}%'
|
||||
}),
|
||||
use_container_width=True
|
||||
)
|
||||
else:
|
||||
st.warning("暂无汇总数据")
|
||||
|
||||
# ============ Tab 2: 收益分析 ============
|
||||
with tab2:
|
||||
st.header("收益分析")
|
||||
|
||||
if trades_df is not None:
|
||||
# 按月份统计收益
|
||||
trades_df['年月'] = trades_df['出场日期'].dt.to_period('M')
|
||||
monthly_returns = trades_df.groupby('年月').agg({
|
||||
'持仓收益': 'sum',
|
||||
'品种代码': 'count'
|
||||
}).rename(columns={'品种代码': '交易次数'})
|
||||
monthly_returns.index = monthly_returns.index.astype(str)
|
||||
|
||||
# 月度收益柱状图
|
||||
fig = px.bar(
|
||||
monthly_returns.reset_index(),
|
||||
x='年月',
|
||||
y='持仓收益',
|
||||
title="月度累计收益",
|
||||
labels={'持仓收益': '收益 (%)', '年月': '月份'},
|
||||
color='持仓收益',
|
||||
color_continuous_scale=['red', 'yellow', 'green']
|
||||
)
|
||||
st.plotly_chart(fig, use_container_width=True)
|
||||
|
||||
# 收益分布直方图
|
||||
fig2 = px.histogram(
|
||||
trades_df,
|
||||
x='持仓收益',
|
||||
nbins=50,
|
||||
title="单次交易收益分布",
|
||||
labels={'持仓收益': '收益 (%)', 'count': '次数'}
|
||||
)
|
||||
fig2.add_vline(x=0, line_dash="dash", line_color="red")
|
||||
st.plotly_chart(fig2, use_container_width=True)
|
||||
|
||||
# 累计收益曲线
|
||||
trades_sorted = trades_df.sort_values('出场日期')
|
||||
trades_sorted['累计收益_曲线'] = trades_sorted['持仓收益'].cumsum()
|
||||
|
||||
fig3 = px.line(
|
||||
trades_sorted,
|
||||
x='出场日期',
|
||||
y='累计收益_曲线',
|
||||
title="策略累计收益曲线",
|
||||
labels={'累计收益_曲线': '累计收益 (%)', '出场日期': '日期'}
|
||||
)
|
||||
st.plotly_chart(fig3, use_container_width=True)
|
||||
else:
|
||||
st.warning("暂无交易数据")
|
||||
|
||||
# ============ Tab 3: 调仓记录 ============
|
||||
with tab3:
|
||||
st.header("调仓记录")
|
||||
|
||||
if trades_df is not None:
|
||||
# 筛选器
|
||||
col1, col2, col3 = st.columns(3)
|
||||
|
||||
with col1:
|
||||
selected_codes = st.multiselect(
|
||||
"选择品种",
|
||||
options=trades_df['品种代码'].unique(),
|
||||
default=[]
|
||||
)
|
||||
|
||||
with col2:
|
||||
min_return = st.slider(
|
||||
"最小收益",
|
||||
min_value=float(trades_df['持仓收益'].min()),
|
||||
max_value=float(trades_df['持仓收益'].max()),
|
||||
value=float(trades_df['持仓收益'].min())
|
||||
)
|
||||
|
||||
with col3:
|
||||
min_days = st.slider(
|
||||
"最小持仓天数",
|
||||
min_value=1,
|
||||
max_value=int(trades_df['持仓天数'].max()),
|
||||
value=1
|
||||
)
|
||||
|
||||
# 过滤数据
|
||||
filtered_df = trades_df.copy()
|
||||
if selected_codes:
|
||||
filtered_df = filtered_df[filtered_df['品种代码'].isin(selected_codes)]
|
||||
filtered_df = filtered_df[
|
||||
(filtered_df['持仓收益'] >= min_return) &
|
||||
(filtered_df['持仓天数'] >= min_days)
|
||||
]
|
||||
|
||||
# 显示表格
|
||||
st.subheader(f"筛选结果 ({len(filtered_df)} 条记录)")
|
||||
|
||||
# 添加颜色标记
|
||||
def color_return(val):
|
||||
color = 'green' if val > 0 else 'red'
|
||||
return f'color: {color}'
|
||||
|
||||
styled_df = filtered_df.style.applymap(
|
||||
color_return,
|
||||
subset=['持仓收益']
|
||||
).format({
|
||||
'进场价格': '{:.2f}',
|
||||
'出场价格': '{:.2f}',
|
||||
'持仓收益': '{:.2f}%',
|
||||
'进场净值': '{:.4f}',
|
||||
'出场净值': '{:.4f}',
|
||||
'净值贡献': '{:.4f}'
|
||||
})
|
||||
|
||||
st.dataframe(styled_df, use_container_width=True, height=500)
|
||||
|
||||
# 导出按钮
|
||||
csv = filtered_df.to_csv(index=False).encode('utf-8')
|
||||
st.download_button(
|
||||
label="📥 导出筛选结果",
|
||||
data=csv,
|
||||
file_name=f"trades_filtered_{datetime.now().strftime('%Y%m%d')}.csv",
|
||||
mime="text/csv"
|
||||
)
|
||||
else:
|
||||
st.warning("暂无调仓记录")
|
||||
|
||||
# ============ Tab 4: 品种详情 ============
|
||||
with tab4:
|
||||
st.header("品种详情分析")
|
||||
|
||||
if summary_df is not None and trades_df is not None:
|
||||
selected_code = st.selectbox(
|
||||
"选择品种",
|
||||
options=summary_df['品种代码'].tolist(),
|
||||
format_func=lambda x: f"{x} - {summary_df[summary_df['品种代码']==x]['品种名称'].iloc[0]}"
|
||||
)
|
||||
|
||||
if selected_code:
|
||||
# 品种基本信息
|
||||
code_info = summary_df[summary_df['品种代码'] == selected_code].iloc[0]
|
||||
|
||||
col1, col2, col3, col4 = st.columns(4)
|
||||
with col1:
|
||||
st.metric("调仓次数", int(code_info['调仓次数']))
|
||||
with col2:
|
||||
st.metric("胜率", f"{code_info['胜率']:.2f}%")
|
||||
with col3:
|
||||
st.metric("累计收益", f"{code_info['累计收益']:.2f}%")
|
||||
with col4:
|
||||
st.metric("平均持仓天数", f"{code_info['平均持仓天数']:.1f}天")
|
||||
|
||||
# 该品种的交易记录
|
||||
code_trades = trades_df[trades_df['品种代码'] == selected_code].sort_values('出场日期')
|
||||
|
||||
if len(code_trades) > 0:
|
||||
# 收益时间序列
|
||||
code_trades['累计收益_品种'] = code_trades['持仓收益'].cumsum()
|
||||
|
||||
fig = make_subplots(
|
||||
rows=2, cols=1,
|
||||
subplot_titles=('单次收益', '累计收益'),
|
||||
vertical_spacing=0.1
|
||||
)
|
||||
|
||||
fig.add_trace(
|
||||
go.Bar(
|
||||
x=code_trades['出场日期'],
|
||||
y=code_trades['持仓收益'],
|
||||
name='单次收益',
|
||||
marker_color=['green' if x > 0 else 'red' for x in code_trades['持仓收益']]
|
||||
),
|
||||
row=1, col=1
|
||||
)
|
||||
|
||||
fig.add_trace(
|
||||
go.Scatter(
|
||||
x=code_trades['出场日期'],
|
||||
y=code_trades['累计收益_品种'],
|
||||
name='累计收益',
|
||||
mode='lines+markers'
|
||||
),
|
||||
row=2, col=1
|
||||
)
|
||||
|
||||
fig.update_layout(height=600, showlegend=False)
|
||||
st.plotly_chart(fig, use_container_width=True)
|
||||
|
||||
# 持仓天数分布
|
||||
fig2 = px.box(
|
||||
code_trades,
|
||||
y='持仓天数',
|
||||
title=f"{code_info['品种名称']} 持仓天数分布"
|
||||
)
|
||||
st.plotly_chart(fig2, use_container_width=True)
|
||||
else:
|
||||
st.info("该品种暂无交易记录")
|
||||
else:
|
||||
st.warning("暂无数据")
|
||||
|
||||
# 底部信息
|
||||
st.markdown("---")
|
||||
st.markdown("""
|
||||
<div style="text-align: center; color: #666;">
|
||||
<p>ETF轮动策略回测系统 | 数据更新时间: {}</p>
|
||||
</div>
|
||||
""".format(datetime.now().strftime('%Y-%m-%d %H:%M:%S')), unsafe_allow_html=True)
|
||||
Reference in New Issue
Block a user