app.py
"""
QTrading Web GUI 应用模块
"""
import dash
from dash import dcc, html, Input, Output, State, dash_table
import dash_bootstrap_components as dbc
import pandas as pd
from typing import List, Dict, Optional
from data.data_manager import DataManager
from engine.selector_engine import SelectorEngine
from backtest.backtest_engine import BacktestEngine
from strategy.strategy_manager import StrategyManager
from config.config import config, ADJUST_METHOD
from utils.logger import get_logger
logger = get_logger(__name__)
# 初始化应用
app = dash.Dash(
__name__,
external_stylesheets=[dbc.themes.BOOTSTRAP],
suppress_callback_exceptions=True,
title="QTrading 量化选股系统"
)
app.layout = dbc.Container(
fluid=True,
children=[
# 导航栏
dbc.Navbar(
[
html.A(
dbc.Row(
[
dbc.Col(html.Img(src="", height="30px")),
dbc.Col(dbc.NavbarBrand("QTrading 量化选股系统", className="ms-2")),
],
align="center",
className="g-0",
),
href="#",
style={"textDecoration": "none"},
),
],
color="dark",
dark=True,
className="mb-4",
),
# 主内容区域
dbc.Row([
# 侧边栏
dbc.Col(
width=3,
children=[
dbc.Card(
[
dbc.CardHeader("参数配置"),
dbc.CardBody([
# 策略选择
html.H6("策略选择"),
dcc.Dropdown(
id="strategy-selector",
options=[],
value=config.strategy.default_strategy,
clearable=False,
className="mb-3",
),
# 复权方式
html.H6("复权方式"),
dcc.Dropdown(
id="adjust-method-selector",
options=[
{"label": value, "value": key}
for key, value in ADJUST_METHOD.items()
],
value="qfq",
clearable=False,
className="mb-3",
),
# 选股数量
html.H6("选股数量"),
dcc.Slider(
id="top-n-selector",
min=5,
max=50,
step=5,
value=10,
marks={i: str(i) for i in range(5, 51, 5)},
className="mb-3",
),
# 操作按钮
dbc.ButtonGroup(
[
dbc.Button(
"开始选股",
id="start-select-btn",
color="primary",
size="lg",
className="me-2",
),
dbc.Button(
"更新数据",
id="update-data-btn",
color="secondary",
size="lg",
),
],
className="w-100",
),
# 回测配置
html.Hr(),
html.H6("回测配置"),
dbc.Input(
id="backtest-start-date",
type="text",
placeholder="开始日期 (YYYYMMDD)",
value="20200101",
className="mb-2",
),
dbc.Input(
id="backtest-end-date",
type="text",
placeholder="结束日期 (YYYYMMDD)",
value="20241231",
className="mb-2",
),
dbc.Button(
"开始回测",
id="start-backtest-btn",
color="success",
size="lg",
className="w-100",
),
]),
],
className="h-100",
),
],
),
# 主内容区
dbc.Col(
width=9,
children=[
# 选股结果
dbc.Card(
[
dbc.CardHeader("选股结果"),
dbc.CardBody([
html.Div(id="stock-select-results"),
]),
],
className="mb-4",
),
# 策略表现
dbc.Card(
[
dbc.CardHeader("策略表现"),
dbc.CardBody([
html.Div(id="strategy-performance"),
]),
],
className="mb-4",
),
# 回测结果
dbc.Card(
[
dbc.CardHeader("回测结果"),
dbc.CardBody([
html.Div(id="backtest-results"),
]),
],
className="mb-4",
),
# 图表
dbc.Card(
[
dbc.CardHeader("图表分析"),
dbc.CardBody([
dcc.Graph(id="stock-chart"),
]),
],
className="mb-4",
),
],
),
]),
],
)
# 初始化组件
selector_engine = SelectorEngine()
backtest_engine = BacktestEngine()
strategy_manager = StrategyManager()
data_manager = DataManager()
# 回调函数:加载策略列表
@app.callback(
Output("strategy-selector", "options"),
Input("strategy-selector", "id")
)
def load_strategy_options(_):
"""加载策略列表到下拉菜单"""
strategies = strategy_manager.list_strategies()
options = [
{"label": f"{strategy['name']} ({strategy['description']})", "value": strategy['name']}
for strategy in strategies
]
return options
# 回调函数:开始选股
@app.callback(
Output("stock-select-results", "children"),
Input("start-select-btn", "n_clicks"),
State("strategy-selector", "value"),
State("adjust-method-selector", "value"),
State("top-n-selector", "value"),
prevent_initial_call=True
)
def start_stock_selection(n_clicks, strategy_name, adjust_method, top_n):
"""开始选股"""
if n_clicks is None:
return html.Div("请点击'开始选股'按钮")
logger.info(f"开始选股:策略={strategy_name},复权方式={adjust_method},选股数量={top_n}")
try:
# 执行选股
results = selector_engine.select_top_n(
strategy_name=strategy_name,
n=top_n,
adjust_method=adjust_method
)
if not results:
return dbc.Alert("选股失败,请检查数据和策略配置", color="danger")
# 转换结果为 DataFrame
df = pd.DataFrame(results)
# 显示选股结果
table = dash_table.DataTable(
data=df.to_dict("records"),
columns=[
{"name": "股票代码", "id": "code"},
{"name": "交易日", "id": "trade_date"},
{"name": "收盘价", "id": "close"},
{"name": "成交量", "id": "vol"},
{"name": "得分", "id": "score"}
],
style_table={"overflowX": "auto"},
style_cell={"textAlign": "center"},
style_header={"fontWeight": "bold"},
sort_action="native",
sort_mode="single",
filter_action="native",
page_action="native",
page_current=0,
page_size=10,
)
return table
except Exception as e:
logger.error(f"选股失败:{e}")
return dbc.Alert(f"选股失败:{str(e)}", color="danger")
# 回调函数:更新数据
@app.callback(
Output("strategy-performance", "children"),
Input("update-data-btn", "n_clicks"),
prevent_initial_call=True
)
def update_stock_data(n_clicks):
"""更新股票数据"""
if n_clicks is None:
return html.Div("")
logger.info("开始更新股票数据")
try:
# 更新数据
data_manager.update_all_stocks()
# 获取数据覆盖情况
coverage = selector_engine.get_data_coverage()
coverage_info = dbc.Card(
[
dbc.CardHeader("数据覆盖情况"),
dbc.CardBody([
html.P(f"股票总数: {coverage['total_stocks']}"),
html.P(f"有效数据: {coverage['has_data_count']}"),
html.P(f"无效数据: {coverage['no_data_count']}"),
html.P(f"覆盖率: {coverage['coverage_rate']}%"),
]),
]
)
return coverage_info
except Exception as e:
logger.error(f"数据更新失败:{e}")
return dbc.Alert(f"数据更新失败:{str(e)}", color="danger")
# 回调函数:开始回测
@app.callback(
Output("backtest-results", "children"),
Input("start-backtest-btn", "n_clicks"),
State("strategy-selector", "value"),
State("adjust-method-selector", "value"),
State("backtest-start-date", "value"),
State("backtest-end-date", "value"),
prevent_initial_call=True
)
def start_backtest(n_clicks, strategy_name, adjust_method, start_date, end_date):
"""开始回测"""
if n_clicks is None:
return html.Div("")
logger.info(f"开始回测:策略={strategy_name},复权方式={adjust_method},日期={start_date}-{end_date}")
try:
# 执行回测
results = backtest_engine.backtest_strategy(
strategy_name=strategy_name,
adjust_method=adjust_method,
start_date=start_date,
end_date=end_date
)
if not results:
return dbc.Alert("回测失败,请检查参数配置", color="danger")
# 显示回测结果
result_info = dbc.Card(
[
dbc.CardHeader("回测结果"),
dbc.CardBody([
html.P(f"总收益率: {results['total_return']}%"),
html.P(f"年化收益率: {results['annual_return']}%"),
html.P(f"最大回撤: {results['max_drawdown']}%"),
html.P(f"年化波动率: {results['annual_volatility']}%"),
html.P(f"夏普比率: {results['sharpe_ratio']}"),
html.P(f"胜率: {results['win_rate']}%"),
html.P(f"盈亏比: {results['profit_loss_ratio']}"),
html.P(f"交易天数: {results['total_trading_days']}"),
]),
]
)
# 绘制净值曲线
nav_data = pd.DataFrame({
'日期': results['date_series'],
'净值': results['nav_series']
})
nav_chart = dcc.Graph(
id="nav-chart",
figure={
"data": [
{"x": nav_data["日期"], "y": nav_data["净值"], "type": "line", "name": "策略净值"}
],
"layout": {
"title": "策略净值曲线",
"xaxis": {"title": "日期"},
"yaxis": {"title": "净值"},
"height": 300
}
},
)
return html.Div([result_info, nav_chart])
except Exception as e:
logger.error(f"回测失败:{e}")
return dbc.Alert(f"回测失败:{str(e)}", color="danger")
# 回调函数:显示股票图表
@app.callback(
Output("stock-chart", "figure"),
Input("stock-select-results", "children"),
State("adjust-method-selector", "value"),
prevent_initial_call=True
)
def display_stock_chart(children, adjust_method):
"""显示选中股票的图表"""
if not children or "dash_table" not in str(children):
return {
"data": [],
"layout": {
"title": "请先执行选股以查看股票图表",
"xaxis": {"title": "日期"},
"yaxis": {"title": "价格"},
"height": 400
}
}
# 简单返回一个示例图表
return {
"data": [
{
"x": ["2024-01-01", "2024-01-02", "2024-01-03"],
"y": [100, 105, 102],
"type": "line",
"name": "示例股票"
}
],
"layout": {
"title": "股票价格走势(示例数据)",
"xaxis": {"title": "日期"},
"yaxis": {"title": "价格"},
"height": 400
}
}
# 运行应用
def run_app(host: str = None, port: int = None, debug: bool = None):
"""运行 Web 应用"""
if host is None:
host = config.web.host
if port is None:
port = config.web.port
if debug is None:
debug = config.web.debug
logger.info(f"启动 QTrading Web 应用: {host}:{port}")
app.run_server(host=host, port=port, debug=debug)
if __name__ == "__main__":
run_app()