Qionk/a-share-quant
0
1"""2A股量化辅助决策仪表盘3启动: streamlit run app.py4"""5 6import streamlit as st7import pandas as pd8import numpy as np9import plotly.graph_objects as go10from src.data import load_config, load_panel_data, load_index_data, get_stock_pool11from src.factors import compute_all_factors, evaluate_factors12from src.signal import generate_signals13from src.backtest import backtest, calc_metrics14 15st.set_page_config(page_title="A股量化辅助决策", layout="wide")16st.title("A股量化辅助决策仪表盘")17 18 19# ── 数据加载(带缓存)──────────────────────────────────────20 21 22@st.cache_data(ttl=3600)23def load_all():24 config = load_config()25 pool = get_stock_pool(config)26 panel = load_panel_data(config, codes=pool["code"].tolist())27 index_data = load_index_data(config)28 return config, pool, panel, index_data29 30 31try:32 config, pool, panel, index_data = load_all()33except Exception as e:34 st.error(f"数据加载失败: {e}")35 st.info("请先运行 `python run.py update` 获取数据")36 st.stop()37 38if not panel:39 st.warning("无数据,请先运行 `python run.py update`")40 st.stop()41 42factors, breadth = compute_all_factors(panel, config)43signals, scores, regime = generate_signals(factors, breadth, config)44 45 46# ── 页签 ─────────────────────────────────────────────────────47 48tab1, tab2, tab3, tab4 = st.tabs(["今日信号", "回测分析", "因子检验", "市场情绪"])49 50 51# ═══════ Tab 1: 今日信号 ═══════52 53with tab1:54 latest = scores.index[-1]55 st.subheader(f"{latest.strftime('%Y-%m-%d')} 关注池")56 57 # 市场状态58 c1, c2 = st.columns(2)59 regime_map = {"normal": "正常", "caution": "谨慎", "bear": "回避"}60 c1.metric("市场状态", regime_map.get(regime.iloc[-1], regime.iloc[-1]))61 c2.metric("市场宽度", f"{breadth.iloc[-1]:.1%}")62 63 # Top N64 today_scores = scores.loc[latest].dropna().sort_values(ascending=False)65 top = today_scores.head(config["signal"]["top_n"])66 67 rows = []68 for code in top.index:69 rows.append({70 "代码": code,71 "综合评分": f"{top[code]:.3f}",72 "相对强度": f'{factors["relative_strength"].loc[latest].get(code, np.nan):.2f}',73 "趋势得分": f'{factors["trend"].loc[latest].get(code, np.nan):.2f}',74 "信号确认": "是" if signals.loc[latest].get(code, False) else "否",75 })76 st.dataframe(pd.DataFrame(rows), use_container_width=True, hide_index=True)77 78 # 单只股票 K 线79 st.subheader("个股走势")80 selected = st.selectbox("选择股票", top.index.tolist())81 if selected and selected in panel["close"].columns:82 price = panel["close"][selected].dropna().tail(120)83 fig = go.Figure(go.Scatter(x=price.index, y=price.values, mode="lines", name=selected))84 for w in config["factors"]["ma_windows"]:85 ma = price.rolling(w).mean()86 fig.add_trace(go.Scatter(x=ma.index, y=ma.values, mode="lines",87 name=f"MA{w}", line=dict(dash="dash")))88 fig.update_layout(title=f"{selected} 近 120 日走势", height=400,89 xaxis_title="日期", yaxis_title="价格(后复权)")90 st.plotly_chart(fig, use_container_width=True)91 92 93# ═══════ Tab 2: 回测分析 ═══════94 95with tab2:96 st.subheader("策略回测")97 98 results = backtest(signals, scores, panel["close"], config)99 100 if not results["daily_returns"].empty:101 metrics = calc_metrics(results["daily_returns"])102 103 # 指标卡片104 cols = st.columns(4)105 items = list(metrics.items())106 for i, (k, v) in enumerate(items[:4]):107 cols[i].metric(k, v)108 if len(items) > 4:109 cols2 = st.columns(4)110 for i, (k, v) in enumerate(items[4:8]):111 cols2[i].metric(k, v)112 113 # 净值曲线114 fig = go.Figure()115 fig.add_trace(go.Scatter(116 x=results["daily_returns"].index,117 y=results["daily_returns"]["value"],118 mode="lines", name="策略净值",119 ))120 fig.update_layout(title="策略净值曲线", height=400,121 xaxis_title="日期", yaxis_title="净值")122 st.plotly_chart(fig, use_container_width=True)123 124 # 回撤曲线125 val = results["daily_returns"]["value"]126 dd = (val - val.cummax()) / val.cummax()127 fig2 = go.Figure()128 fig2.add_trace(go.Scatter(129 x=dd.index, y=dd.values,130 fill="tozeroy", fillcolor="rgba(255,0,0,0.1)",131 line=dict(color="red"), name="回撤",132 ))133 fig2.update_layout(title="回撤曲线", height=300,134 xaxis_title="日期", yaxis_title="回撤")135 st.plotly_chart(fig2, use_container_width=True)136 137 # 交易日志138 if not results["trade_log"].empty:139 with st.expander("交易日志(最近 50 条)"):140 st.dataframe(results["trade_log"].tail(50), use_container_width=True, hide_index=True)141 else:142 st.warning("回测数据不足")143 144 145# ═══════ Tab 3: 因子检验 ═══════146 147with tab3:148 st.subheader("因子 Rank IC 分析")149 150 ic_results = evaluate_factors(factors, panel["close"])151 152 if ic_results:153 summary = pd.DataFrame({154 name: {155 "IC 均值": f'{r["ic_mean"]:.4f}',156 "IC 标准差": f'{r["ic_std"]:.4f}',157 "ICIR": f'{r["icir"]:.4f}',158 "IC>0 占比": f'{r["ic_positive_pct"]:.1%}',159 }160 for name, r in ic_results.items()161 }).T162 st.dataframe(summary, use_container_width=True)163 164 sel = st.selectbox("选择因子查看 IC 时序", list(ic_results.keys()))165 if sel:166 ic_s = ic_results[sel]["ic_series"]167 fig = go.Figure(go.Bar(x=ic_s.index, y=ic_s.values, name="IC"))168 fig.add_hline(y=ic_s.mean(), line_dash="dash", line_color="red",169 annotation_text=f"均值: {ic_s.mean():.4f}")170 fig.update_layout(title=f"{sel} - IC 时序", height=400,171 xaxis_title="日期", yaxis_title="IC")172 st.plotly_chart(fig, use_container_width=True)173 else:174 st.info("因子数据不足,无法评估")175 176 177# ═══════ Tab 4: 市场情绪 ═══════178 179with tab4:180 st.subheader("市场宽度指标")181 182 fig = go.Figure()183 fig.add_trace(go.Scatter(x=breadth.index, y=breadth.values, mode="lines", name="市场宽度"))184 fig.add_hline(y=config["market"]["breadth_threshold_high"],185 line_dash="dash", line_color="green", annotation_text="多头阈值")186 fig.add_hline(y=config["market"]["breadth_threshold_low"],187 line_dash="dash", line_color="red", annotation_text="空头阈值")188 fig.update_layout(title="市场宽度(站上 20 日均线股票占比)", height=400,189 xaxis_title="日期", yaxis_title="占比")190 st.plotly_chart(fig, use_container_width=True)191 192 # 指数 K 线193 if not index_data.empty:194 fig2 = go.Figure(go.Candlestick(195 x=index_data.index,196 open=index_data["open"], high=index_data["high"],197 low=index_data["low"], close=index_data["close"],198 name=config["market"]["index_code"],199 ))200 fig2.update_layout(201 title=f'指数走势 ({config["market"]["index_code"]})',202 height=450, xaxis_rangeslider_visible=False,203 )204 st.plotly_chart(fig2, use_container_width=True)205 