""" 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(""" """, unsafe_allow_html=True) # 标题 st.markdown('

📈 ETF轮动策略回测结果

', 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("""

ETF轮动策略回测系统 | 数据更新时间: {}

""".format(datetime.now().strftime('%Y-%m-%d %H:%M:%S')), unsafe_allow_html=True)