|
| 1 | +"""Streamlit entry-point for the trading dashboard.""" |
| 2 | +from __future__ import annotations |
| 3 | + |
| 4 | +import pandas as pd |
| 5 | +import streamlit as st |
| 6 | + |
| 7 | +from .executor import PaperTradeExecutor |
| 8 | +from .fetcher import MockMarketDataFetcher |
| 9 | +from .llm_client import LLMClient |
| 10 | +from .prompt_builder import PromptBuilder |
| 11 | + |
| 12 | +st.set_page_config(page_title="Trading Dashboard", layout="wide", page_icon="📈") |
| 13 | + |
| 14 | + |
| 15 | +def _init_state() -> None: |
| 16 | + if "fetcher" not in st.session_state: |
| 17 | + st.session_state.fetcher = MockMarketDataFetcher() |
| 18 | + if "executor" not in st.session_state: |
| 19 | + st.session_state.executor = PaperTradeExecutor() |
| 20 | + if "llm_client" not in st.session_state: |
| 21 | + st.session_state.llm_client = LLMClient() |
| 22 | + if "prompt_builder" not in st.session_state: |
| 23 | + st.session_state.prompt_builder = PromptBuilder() |
| 24 | + |
| 25 | + |
| 26 | +def _refresh_snapshot() -> None: |
| 27 | + fetcher: MockMarketDataFetcher = st.session_state.fetcher |
| 28 | + executor: PaperTradeExecutor = st.session_state.executor |
| 29 | + llm_client: LLMClient = st.session_state.llm_client |
| 30 | + prompt_builder: PromptBuilder = st.session_state.prompt_builder |
| 31 | + |
| 32 | + market = fetcher.fetch_latest() |
| 33 | + account = executor.get_account_state(market.price) |
| 34 | + prompt = prompt_builder.build_prompt(market, account) |
| 35 | + decision = llm_client.get_trading_decision(prompt) |
| 36 | + |
| 37 | + st.session_state.market = market |
| 38 | + st.session_state.account = account |
| 39 | + st.session_state.prompt = prompt |
| 40 | + st.session_state.decision = decision |
| 41 | + |
| 42 | + |
| 43 | +def _render_market_panel() -> None: |
| 44 | + market = st.session_state.market |
| 45 | + st.subheader("🔹 行情面板") |
| 46 | + cols = st.columns(5) |
| 47 | + cols[0].metric("价格", f"{market.price:.2f}") |
| 48 | + cols[1].metric("EMA(20)", f"{market.ema:.2f}") |
| 49 | + cols[2].metric("MACD", f"{market.macd:.4f}") |
| 50 | + cols[3].metric("MACD Signal", f"{market.macd_signal:.4f}") |
| 51 | + cols[4].metric("RSI", f"{market.rsi:.2f}") |
| 52 | + |
| 53 | + st.metric("成交量", f"{market.volume:,.0f}") |
| 54 | + |
| 55 | + history = market.history.copy() |
| 56 | + history.set_index("time", inplace=True) |
| 57 | + history.index = pd.to_datetime(history.index) |
| 58 | + st.line_chart(history["close"], height=240) |
| 59 | + |
| 60 | + |
| 61 | +def _render_account_panel() -> None: |
| 62 | + account = st.session_state.account |
| 63 | + st.subheader("🔹 账户面板") |
| 64 | + cols = st.columns(3) |
| 65 | + cols[0].metric("账户价值", f"{account.total_value:,.2f}") |
| 66 | + cols[1].metric("现金", f"{account.cash:,.2f}") |
| 67 | + positions = ", ".join(f"{symbol}: {qty}" for symbol, qty in account.positions.items()) or "无" |
| 68 | + cols[2].metric("持仓", positions) |
| 69 | + |
| 70 | + |
| 71 | +def _render_ai_panel() -> None: |
| 72 | + st.subheader("🔹 AI 决策展示") |
| 73 | + st.markdown( |
| 74 | + "当前策略建议:" |
| 75 | + f"<span style='font-size:28px; font-weight:bold;'> {st.session_state.decision}</span>", |
| 76 | + unsafe_allow_html=True, |
| 77 | + ) |
| 78 | + with st.expander("查看 Prompt"): |
| 79 | + st.code(st.session_state.prompt) |
| 80 | + |
| 81 | + |
| 82 | +def _render_order_panel() -> None: |
| 83 | + st.subheader("🔹 模拟下单") |
| 84 | + market = st.session_state.market |
| 85 | + executor: PaperTradeExecutor = st.session_state.executor |
| 86 | + action = st.selectbox( |
| 87 | + "选择操作", |
| 88 | + ["Buy", "Sell", "Hold"], |
| 89 | + index=["Buy", "Sell", "Hold"].index(st.session_state.decision), |
| 90 | + ) |
| 91 | + if st.button("执行下单"): |
| 92 | + result = executor.execute(action, market) |
| 93 | + st.success(f"{result.message} 数量: {result.quantity}, 价格: {result.price:.2f}") |
| 94 | + st.session_state.account = executor.get_account_state(market.price) |
| 95 | + |
| 96 | + if executor.order_history: |
| 97 | + st.markdown("**订单历史**") |
| 98 | + history_data = [ |
| 99 | + { |
| 100 | + "时间": order.timestamp.strftime("%H:%M:%S"), |
| 101 | + "操作": order.action, |
| 102 | + "数量": order.quantity, |
| 103 | + "价格": f"{order.price:.2f}", |
| 104 | + "备注": order.message, |
| 105 | + } |
| 106 | + for order in reversed(executor.order_history[-10:]) |
| 107 | + ] |
| 108 | + st.table(history_data) |
| 109 | + |
| 110 | + |
| 111 | +def main() -> None: |
| 112 | + st.markdown("<meta http-equiv='refresh' content='180'>", unsafe_allow_html=True) |
| 113 | + st.title("🚀 AI Trading Dashboard") |
| 114 | + st.caption("最小可行闭环:实时行情 + 账户状态 + AI 策略 + 一键下单") |
| 115 | + |
| 116 | + _init_state() |
| 117 | + |
| 118 | + _refresh_snapshot() |
| 119 | + |
| 120 | + if st.button("手动刷新"): |
| 121 | + _refresh_snapshot() |
| 122 | + |
| 123 | + col1, col2 = st.columns([2, 1]) |
| 124 | + with col1: |
| 125 | + _render_market_panel() |
| 126 | + with col2: |
| 127 | + _render_account_panel() |
| 128 | + |
| 129 | + col3, col4 = st.columns(2) |
| 130 | + with col3: |
| 131 | + _render_ai_panel() |
| 132 | + with col4: |
| 133 | + _render_order_panel() |
| 134 | + |
| 135 | + |
| 136 | +if __name__ == "__main__": |
| 137 | + main() |
0 commit comments