[升级]21天搭建ETF量化交易系统DAY19—“实盘明细”页面接入委托明细数据

升级说明

回顾下前几次升级的内容:

我们把backtrader回测框架集成到系统里面,实现了真实的历史数据回测轮动策略。

[升级]21天搭建ETF量化交易系统Day19—Web版轮动系统,深度集成Backtrader回测!

我们实现一键更新行情数据到本地缓存,回测直接调用本地数据,大幅提升策略回测速度和稳定性。

[升级]21天搭建ETF量化交易系统DAY19—Web版轮动系统,完成本地数据管理模块!

我们打通miniQMT接口,可以直接通过轮动池页面,点击ETF品种直接下单。

[升级]21天搭建ETF量化交易系统DAY19—Web版轮动系统,打通miniQMT下单接口!

我们把QMT账户数据同步至系统“我的账户”,涵盖总资产、持仓市值及明细,并新增了持仓市值分布与收益率排行图表。

[升级]21天搭建ETF量化交易系统DAY19—多进程架构!“我的持仓”同步QMT账户信息!


我的轮动池已接入真实行情:ETF动量基于真实数据计算并实时排序,颜色高亮区分强弱。点击右侧“详情”按钮,可一键买入/卖出排名靠前的品种,系统自动获取实时价格并校验资金持仓。

[升级]21天搭建ETF量化交易系统DAY19—动量排名实盘接入,一键轻松下单!

本次升级:“实盘明细”页面正式接入QMT真实委托数据。系统可实时查询当日委托记录,完整展示委托时间、证券代码、证券名称、买卖方向、委托状态、委托量、成交数量、已撤数量、委托价格、成交均价、冻结资金、订单编号及废单原因等全部字段。支持手动刷新,按需获取最新委托状态,让每一笔交易全程可追溯、状态清晰可见。

图片

搭建过程

图片

这个系列,我们用Python从0一步步搭建出一套ETF量化交易系统(选择ETF标的是因为对于普通交易者来说,ETF相对于选强势股难度要小,而且没有退市风险)。大家可以跟随着我们的实现路径来一起学习,从过程中掌握方法。

掌握了方法之后,可以换成期货系统、比特币系统、美股系统,然后在实战中不断去完善自己的系统了。

DAY1-DAY21链接如下:21天搭建ETF量化交易系统

我们与时俱进,还会持续对现有的DAY21的内容升级!

页面功能展示我的轮动池:展示ETF列表,按动量分数实时排序,不同排名用颜色区分。支持搜索ETF,点击“详情”按钮可对该ETF进行买入/卖出操作。


我的账户:展示总资产、持仓市值、可用资金、当前盈亏四大核心数据。通过饼图展示持仓市值分布,柱状图展示持仓收益率排行,表格展示详细持仓明细。
策略回测:设置回测日期、初始资金、动量周期、调仓周期、持有数量等参数,点击开始回测后运行Backtrader引擎,展示总收益率、夏普比率、最大回撤、胜率、交易次数、盈亏比等指标,并生成净值曲线图。



参数配置:配置策略参数(动量周期、调仓周期、持有数量)、交易参数(手续费率、滑点、最小交易单位)、风险参数(止损止盈、回撤警戒、仓位限制等)。实盘明细:点击刷新按钮获取QMT当日委托记录,展示委托时间、证券代码、证券名称、买卖方向、委托状态、委托量、成交数量、已撤数量、委托价格、成交均价、冻结资金、订单编号、废单原因等信息。数据管理:点击“立即更新”按钮,遍历所有ETF从网络获取历史数据并保存到本地“行情数据库”目录,同时更新表格显示每只ETF的数据状态。


系统监控:展示服务器、数据库、策略运行、交易接口四大组件的运行状态;展示CPU和内存使用率曲线图;展示系统运行日志列表。策略文档:展示动量轮动策略的说明、使用指南和注意事项。

动量排名实盘接入

委托明细实盘接入
本次升级在现有QMT多进程架构基础上,新增实盘委托查询与展示功能。用户可通过“实盘明细”页面实时查询当日委托记录,完整了解每笔交易的状态和详情。

整体架构

  
  1. 用户点击导航菜单“实盘明细”

  2. 页面加载,显示空白表格和刷新按钮

  3. 用户点击“🔄 刷新”按钮

  4. Dash回调触发,通过QMT客户端发送get_orders命令

  5. QMT工作进程调用trader.query_stock_orders()获取委托数据

  6. 数据经解析后返回主进程

  7. 表格自动更新,显示当日委托记录

QMTWorker类中实现get_orders方法:

def get_orders(self, cancelable_only=False):    """获取当日委托"""    orders = self.trader.query_stock_orders(self.account)    result = []    for o in orders:        result.append({            # 订单标识            'order_id': str(o.order_id),            'order_sysid': str(o.order_sysid),            # 账户信息            'account_id': str(o.account_id),            # 证券信息            'stock_code': o.stock_code,            'instrument_name': o.instrument_name,  # 证券名称            # 价格数量            'price': float(o.price),            'order_volume': int(o.order_volume),            'traded_volume': int(o.traded_volume),            'traded_price': float(o.traded_price),            # 时间            'order_time': datetime.fromtimestamp(o.order_time).strftime('%Y-%m-%d %H:%M:%S'),            # 类型状态            'direction': '买入' if o.order_type == 23 else '卖出',            'status': parse_order_status(o.order_status),            'status_msg': o.status_msg,  # 废单原因            'is_cancelable': o.order_status in [50, 52, 55]        })    return {'success': True, 'data': result}
前端展示层 - Dash回调
@app.callback(    Output('orders-detail-table', 'data'),    [Input('refresh-orders-btn', 'n_clicks')],    prevent_initial_call=True)def update_orders_table(n_clicks):    """点击刷新按钮时获取委托记录"""    response = qmt_client.get_orders()    orders = response.get('data', [])    table_data = []    for order in orders:        # 只显示当日委托        if order['order_time'].split(' ')[0] != today:            continue        table_data.append({            'order_time': order['order_time'],            'stock_code': order['stock_code'].split('.')[0],            'etf_name': order['instrument_name'],            'direction': order['direction'],            'status': order['status'],            'order_volume': f"{order['order_volume']:,}",            'traded_volume': f"{order['traded_volume']:,}",            'canceled_volume': f"{order['order_volume'] - order['traded_volume']:,}",            'price': f"{order['price']:.3f}",            'traded_price': f"{order['traded_price']:.3f}",            'frozen_amount': f"¥{(order['order_volume'] - order['traded_volume']) * order['price']:,.2f}",            'order_id': order['order_id'],            'status_msg': order['status_msg']        })    return table_data
前端界面 - DataTable配置
dash_table.DataTable(    id='orders-detail-table',    columns=[        {'name': '委托时间', 'id': 'order_time'},        {'name': '证券代码', 'id': 'stock_code'},        {'name': '证券名称', 'id': 'etf_name'},        {'name': '买卖', 'id': 'direction'},        {'name': '委托状态', 'id': 'status'},        {'name': '委托量', 'id': 'order_volume'},        {'name': '成交数量', 'id': 'traded_volume'},        {'name': '已撤数量', 'id': 'canceled_volume'},        {'name': '委托价格', 'id': 'price'},        {'name': '成交均价', 'id': 'traded_price'},        {'name': '冻结资金', 'id': 'frozen_amount'},        {'name': '订单编号', 'id': 'order_id'},        {'name': '废单原因', 'id': 'status_msg'},    ],    style_data_conditional=[        {'if': {'column_id': 'direction', 'filter_query': '{direction} = "买入"'}, 'color': '#e74c3c'},        {'if': {'column_id': 'direction', 'filter_query': '{direction} = "卖出"'}, 'color': '#2ecc71'},        {'if': {'column_id': 'status', 'filter_query': '{status} = "全部成交"'}, 'backgroundColor': '#d4edda'},        {'if': {'column_id': 'status', 'filter_query': '{status} = "部分成交"'}, 'backgroundColor': '#fff3cd'},    ])
动量排名实盘接入

本次升级在现有多进程QMT架构基础上,为“我的轮动池”页面增加了实时行情获取和动量计算功能:

ETF动量计算函数(统一算法)
def calculate_momentum(df, momentum_period=20):    """    核心动量计算函数    :param df: DataFrame,必须包含'close'列    :param momentum_period: 动量周期    :return: 动量分数    """    try:        if df is None or df.empty or len(df) < momentum_period + 1:            return 0        # 确保数据排序        df = df.sort_index()        # 计算动量: (当前价格 - N天前价格) / N天前价格 * 100        current_price = df['close'].iloc[-1]        past_price = df['close'].iloc[-momentum_period - 1]        momentum = (current_price - past_price) / past_price * 100        return round(momentum, 2)    except Exception as e:        print(f"动量计算错误: {e}")        return 0
批量获取ETF历史数据
def get_etf_data(etf_code, start_date=None, end_date=None, use_local=True):    try:        # 1. 优先读取本地        if use_local:            try:                data_dir = "行情数据库"                for filename in os.listdir(data_dir):                    if filename.endswith('.csv') and (                            filename == f"{etf_code}.csv" or f"_{etf_code}.csv" in filename):                        filepath = os.path.join(data_dir, filename)                        df = pd.read_csv(filepath, index_col='date', parse_dates=True)                        if 'open' in df.columns:                            # 按日期过滤                            if start_date and end_date:                                start_dt = pd.to_datetime(start_date)                                end_dt = pd.to_datetime(end_date)                                df = df[(df.index >= start_dt) & (df.index <= end_dt)]                            if not df.empty:                                print(f"📁 从本地读取: {len(df)} 行")                                return df            except:                pass  # 本地读取失败,静默继续        # 2. 从网络获取        if etf_code.startswith(('51', '56', '58')):            symbol = f'sh{etf_code}'        elif etf_code.startswith(('15', '16', '18')):            symbol = f'sz{etf_code}'        else:            # 默认规则:5、6开头的通常是沪市,其他的算深市            if etf_code[0] in ['5', '6']:                symbol = f'sh{etf_code}'            else:                symbol = f'sz{etf_code}'        print(f"🌐 从网络获取: {etf_code}")        df = ak.fund_etf_hist_sina(symbol=symbol)        if df.empty:            print(f"数据为空")            return None        # 数据处理        df = df.rename(columns={'date': 'date', 'open': 'open', 'high': 'high',                                'low': 'low', 'close': 'close', 'volume': 'volume'})        df['date'] = pd.to_datetime(df['date'])        df.set_index('date', inplace=True)        df.sort_index(inplace=True)        # 日期过滤        if start_date and end_date:            start_dt = pd.to_datetime(start_date)            end_dt = pd.to_datetime(end_date)            df = df[(df.index >= start_dt) & (df.index <= end_dt)]        # 数据类型转换        for col in ['open', 'high', 'low', 'close', 'volume']:            df[col] = pd.to_numeric(df[col], errors='coerce')        df = df.dropna()        print(f"  成功: {len(df)} 行数据")        return df    except Exception as e:        print(f"获取 {etf_code} 失败: {e}")        return None

ETF表格生成回调(核心实现)

@app.callback(    Output("etf-pool-table-body", "children"),    [Input("nav-pool", "n_clicks"),     Input("btn-refresh-etf", "n_clicks"),     Input("etf-search-input", "value")],    prevent_initial_call=True)def generate_etf_table(nav_clicks, refresh_clicks, search_text):    """    生成ETF表格,核心功能:    1. 计算所有ETF的动量分数    2. 按动量排序并生成排名    3. 获取实时行情价格    4. 生成带颜色的表格行    """    ctx = dash.callback_context    if not ctx.triggered:        return []    trigger_id = ctx.triggered[0]['prop_id'].split('.')[0]    rows = []    print("计算真实动量...")    momentum_period = 20    # ===== 计算所有ETF的动量 =====    momentum_dict = {}    momentum_list = []    for etf_name, etf_code in IDX_SYM.items():        try:            df = get_etf_data(etf_code, use_local=True)            momentum = calculate_momentum(df, momentum_period)            momentum_dict[etf_code] = momentum            momentum_list.append((etf_code, momentum, etf_name))            if momentum != 0:                print(f"  {etf_name}: 动量={momentum:+.2f}%")        except Exception as e:            print(f"❌ 计算 {etf_name}({etf_code}) 动量失败: {e}")            momentum_dict[etf_code] = 0            momentum_list.append((etf_code, 0, etf_name))    # 按动量排序    sorted_momentum = sorted(momentum_list, key=lambda x: x[1], reverse=True)    rank_map = {code: idx + 1 for idx, (code, _, _) in enumerate(sorted_momentum)}    sorted_items = [(name, code) for code, momentum, name in sorted_momentum]    # 搜索过滤    if search_text and trigger_id == "etf-search-input":        search_text = search_text.lower()        sorted_items = [(name, code) for name, code in sorted_items                        if search_text in name.lower() or search_text in code]    # ===== 批量获取实时行情 =====    try:        # 准备股票代码列表        stock_codes = []        code_to_name = {}        for name, code in sorted_items:            if code.startswith(('51', '56', '58')):                full_code = f"{code}.SH"            elif code.startswith(('15', '16', '18')):                full_code = f"{code}.SZ"            else:                full_code = code            stock_codes.append(full_code)            code_to_name[full_code] = (name, code)        # 从QMT获取实时行情        tick_data = xtdata.get_full_tick(stock_codes)        # 生成表格行        for full_code in stock_codes:            name, code = code_to_name[full_code]            # 获取行情数据            tick = tick_data.get(full_code, {})            current_price = round(tick.get('lastPrice', 0), 3) if tick else 0            # 获取动量分数和排名            momentum_score = momentum_dict.get(code, 0)            rank = rank_map.get(code, len(sorted_items))            # 颜色设置            momentum_color = '#e74c3c' if momentum_score > 0 else '#2ecc71' if momentum_score < 0 else '#f39c12'            if rank <= 5:                rank_color = '#2ecc71'      # 前5名:绿色            elif rank <= 10:                rank_color = '#3498db'      # 6-10名:蓝色            elif rank <= 20:                rank_color = '#f39c12'      # 11-20名:黄色            else:                rank_color = '#e74c3c'      # 20名以后:红色            rows.append(                html.Tr([                    html.Td(dbc.Checkbox(id=f"checkbox-{code}", value=False)),                    html.Td(code, style={'fontWeight': 'bold'}),                    html.Td(name),                    html.Td(f"{current_price:.3f}", style={'fontWeight': 'bold'}),                    html.Td(html.Span(f"{momentum_score:+}",                                      style={'color': momentum_color, 'fontWeight': 'bold'})),                    html.Td(html.Span(f"{momentum_score:+.1f}%",                                      style={'color': momentum_color})),                    html.Td(html.Span(f"{rank}",                                      style={'backgroundColor': rank_color, 'color': 'white',                                             'padding': '3px 8px', 'borderRadius': '3px',                                             'display': 'inline-block', 'minWidth': '30px',                                             'textAlign': 'center'})),                    html.Td(dbc.Button(                        "详情",                        size="sm",                        color="info",                        id={"type": "dynamic-detail-btn", "index": code}                    ))                ])            )    except Exception as e:        print(f"获取行情数据失败: {e}")        # 异常处理:使用模拟数据        for name, code in sorted_items:            current_price = round(np.random.uniform(0.5, 10.0), 3)            momentum_score = momentum_dict.get(code, 0)            rank = rank_map.get(code, len(sorted_items))            # ... 颜色设置和行生成代码同上 ...    return rows
持仓数据同步

系统采用多进程架构来实现QMT的实时同步:

  1. 主进程:Dash Web应用,负责UI展示

  2. 子进程:QMT工作进程,负责与QMT交易系统通信

  3. 进程间通信:通过Queue队列传递数据

QMT多进程客户端 (QMTMultiProcessClient)

class QMTMultiProcessClient:    def __init__(self):        self.cmd_queue = Queue()    # 命令队列:主进程 -> 子进程        self.resp_queue = Queue()   # 响应队列:子进程 -> 主进程        self.process = None          # 子进程对象        self.request_id = 0          # 请求ID,用于匹配响应
工作原理:
  • cmd_queue:主进程发送命令(如"get_account_info")

  • resp_queue:子进程返回结果

  • 每个请求有唯一ID,确保响应匹配正确请求

QMT工作子进程 (qmt_worker_process)

这是一个独立的Python进程,专门负责与QMT系统交互:

def qmt_worker_process(cmd_queue, resp_queue):    class QMTWorker:        def connect(self):            # 连接miniQMT            mini_qmt_path = r"C:\Program Files\国金QMT交易端模拟\userdata_mini"            self.trader = XtQuantTrader(mini_qmt_path, session_id)        def get_account_info(self):            # 从QMT获取账户信息            balance = self.trader.query_stock_asset(self.account)
账户页面布局与数据绑定您的账户页面有3个主要数据展示区域:
<!-- 1. 顶部卡片 - 显示账户汇总信息 --><卡1: 总资产 id="account-total-asset"><卡2: 持仓市值 id="account-position-value"><卡3: 可用资金 id="account-available-cash"><卡4: 当前盈亏 id="account-profit-loss"><!-- 2. 中间图表 - 可视化持仓分布 --><饼图 id="position-pie-chart"><柱状图 id="position-return-bar-chart"><!-- 3. 底部表格 - 详细持仓列表 --><表格 id="position-detail-table">

数据同步的核心回调函数

@app.callback(    [Output(...)],  # 8个输出对应所有UI组件    [Input("nav-account", "n_clicks"),      # 点击导航时触发     Input("account-refresh-interval", "n_intervals")],  # 定时刷新    prevent_initial_call=True)def update_account_all_data(n_clicks, n_intervals):

定时刷新机制

dcc.Interval(    id='account-refresh-interval',    interval=5000,  # 每5秒触发一次    n_intervals=0)

这个组件每5秒触发一次回调,实现自动刷新:

  • n_intervals自动递增

  • 每次递增都会触发update_account_all_data

  • 页面数据实时更新

数据同步流程图

┌─────────────────┐     命令队列     ┌─────────────────┐│   Dash主进程    │ ────────────────> │   QMT子进程     ││                 │                   │                 ││  - Web界面      │                   │  - 连接QMT      ││  - 用户交互     │                   │  - 执行交易操作 ││  - 数据展示     │                   │  - 查询账户     │└─────────────────┘                   └─────────────────┘        ↑                                       │        │                                       │        └───────────────────────────────────────┘                    响应队列
                    时间轴┌─────────────────────────────────────────────────┐│                                                   ││  用户打开账户页面                                  ││  ↓                                                ││  Interval触发回调 (每5秒)                          ││  ↓                                                ││  qmt_client.get_account_info()                    ││  ↓                                                ││  send_command → cmd_queue.put()                   ││  ↓                                                ││  子进程接收命令 → 从QMT查询数据                     ││  ↓                                                ││  子进程返回数据 → resp_queue.put()                 ││  ↓                                                ││  主进程接收数据 → 解析 → 更新UI                     ││  ↓                                                ││  用户看到最新账户数据                              ││                                                   │└─────────────────────────────────────────────────┘

下单模块实现

下单模块是动量策略轮动系统的核心交易执行组件,负责处理从用户界面发起的所有交易指令,与miniQMT交易客户端进行交互,实现ETF的买入、卖出等操作。本模块采用分层设计,包括前端交互层、业务逻辑层和交易执行层。

首先,实现miniQMT交易接口,封装为QmtTrader类。这部分在DAY11中已经完成,直接集成即可。
[升级]21天搭建ETF量化交易系统Day11—添加miniQMT智能拆单算法!
前端组件设计包括交易模态框组件和订单状态提示组件。
交易模态框是用户进行交易操作的主要界面,定义在app.layout中。

dbc.Modal([    dbc.ModalHeader(dbc.ModalTitle("ETF交易下单")),    dbc.ModalBody([        # ETF信息展示        html.H5(id="order-etf-info", className="text-center mb-3"),        # 交易方向选择        dbc.RadioItems(id="order-direction", options=[            {"label": "买入", "value": "buy"},            {"label": "卖出", "value": "sell"}        ], value="buy", inline=True),        # 价格输入        dbc.Input(id="order-current-price", readonly=True),        dbc.Input(id="order-price", type="number", step=0.001),        # 数量输入        dbc.Input(id="order-quantity", type="number", step=100, min=100),        # 金额计算显示        dbc.Input(id="order-amount", readonly=True),        # 资金信息        html.Div([            html.Span("可用资金: "),            html.Span("¥1,234,567.89", id="order-available-cash"),        ]),        # 手续费和冻结金额        html.Div(id="order-commission"),        html.Div(id="order-frozen-amount"),        # 操作按钮        dbc.Button("确认下单", id="order-submit", color="success"),        dbc.Button("取消", id="order-cancel", color="secondary")    ])], id="order-modal", size="lg", is_open=False)
使用Bootstrap Toast组件显示订单提交结果:
dbc.Toast(    id="order-toast",    header="订单提交成功",    is_open=False,    dismissable=True,    duration=4000,    style={"position": "fixed", "top": 80, "right": 20, "width": 350})
业务逻辑层实现包括模态框控制回调、委托金额计算回调、订单提交回调。模态框控制回调:控制交易模态框的打开/关闭,填充ETF信息
@app.callback(    [Output("order-modal", "is_open"),     Output("order-etf-info", "children"),     Output("order-current-price", "value"),     Output("order-price", "value")],    [Input("order-cancel", "n_clicks"),     Input({"type": "dynamic-detail-btn", "index": ALL}, "n_clicks")],    [State("order-modal", "is_open")])def toggle_order_modal(cancel_clicks, detail_clicks, is_open):    # 处理取消按钮点击    if trigger_id == "order-cancel":        return False, "", 0, 0    # 处理详情按钮点击(打开模态框)    if button_dict.get("type") == "dynamic-detail-btn":        etf_code = button_dict.get("index")        etf_name = lookup_etf_name(etf_code)        current_price = get_current_price(etf_code)        return True, f"{etf_name} ({etf_code})", current_price, current_price
委托金额计算回调:实时计算委托金额、手续费和冻结资金
@app.callback(    [Output("order-amount", "value"),     Output("order-commission", "children"),     Output("order-frozen-amount", "children")],    [Input("order-price", "value"),     Input("order-quantity", "value"),     Input("order-direction", "value")])def calculate_order_amount(price, quantity, direction):    if price and quantity and price > 0 and quantity > 0:        amount = price * quantity        commission = amount * 0.0003  # 万3手续费        frozen = amount + commission if direction == "buy" else 0        return (round(amount, 2),                f"¥{commission:.2f}",                f"¥{frozen:.2f}")    return 0, "¥0.00", "¥0.00"

订单提交回调:收集订单信息并调用交易执行层

@app.callback(    [Output("order-modal", "is_open", allow_duplicate=True),     Output("order-toast", "is_open"),     Output("order-toast", "children")],    [Input("order-submit", "n_clicks")],    [State("order-etf-info", "children"),     State("order-direction", "value"),     State("order-price", "value"),     State("order-quantity", "value")])def submit_order(n_clicks, etf_info, direction, price, quantity):    # 解析ETF代码    match = re.search(r'\((\d+)\)', str(etf_info))    etf_code = match.group(1)    # 调用QMT交易接口    order_id = qmt_trader.place_order(        stock_code=etf_code,        price=price,        volume=quantity,        is_buy=(direction == "buy")    )    return False, True, f"订单提交成功!订单号: {order_id}"
数据管理模块实现

当前数据流架构:用户点击"立即更新" → `update_etf_data()`回调函数 → `get_etf_data()`数据获取 → 保存到本地CSV → 更新状态表格

 用户界面交互层

# 在数据管理页面中,用户看到"立即更新"按钮dbc.Button(    "🔄 立即更新",    color="primary",    id="update-now",    className="w-100 h-100",    style={'height': '38px'})
功能说明:
  • 这是用户在界面上的操作入口

  • 按钮ID为update-now,用于关联回调函数

  • 当用户点击此按钮时,触发数据更新流程

回调函数触发机制

@app.callback(    [Output('etf-data-status-table', 'data', allow_duplicate=True),     Output('update-now', 'children', allow_duplicate=True)],    [Input('update-now', 'n_clicks')],    [State('etf-data-status-table', 'data')],    prevent_initial_call=True)

数据更新主函数

def update_etf_data(n_clicks, current_table_data):    """下载ETF最大历史数据范围 - 完全修复版"""    # 1. 确保有点击事件    if n_clicks is None:        print("没有点击事件,阻止更新")        raise dash.exceptions.PreventUpdate

数据获取与处理流程

# 获取数据(不传日期参数获取最大范围)df = get_etf_data(etf_code)  # 关键修改点!if df is not None and not df.empty and len(df) > 0:    # 保存到CSV    filepath = os.path.join(data_dir, f"{etf_name}_{etf_code}.csv")    df.to_csv(filepath, encoding='utf-8-sig', index=True)

核心步骤

  1. 调用get_etf_data(etf_code):获取该ETF所有可用历史数据

  1. 本地存储:文件名格式:{ETF名称}_{ETF代码}.csv,便于识别和管理,保存到"行情数据库"目录,统一管理数据文件

get_etf_data()函数详解

def get_etf_data(etf_code, start_date=None, end_date=None, use_local=True):    """使用akshare获取ETF数据 - 纯读取,不保存"""

函数特点

  • 本地优先策略:先尝试从本地读取(use_local=True

  • 网络备用:本地无数据或数据不足时,从网络获取

  • 优雅降级:网络失败时返回None,不中断整个流程

# 1. 优先读取本地(如果启用)if use_local:    try:        data_dir = "行情数据库"        for filename in os.listdir(data_dir):            if filename.endswith('.csv') and (                    filename == f"{etf_code}.csv" or f"_{etf_code}.csv" in filename):                filepath = os.path.join(data_dir, filename)                df = pd.read_csv(filepath, index_col='date', parse_dates=True)

本地读取逻辑

  1. 遍历"行情数据库"目录下所有CSV文件

  2. 通过文件名匹配(包含ETF代码)

  3. 读取CSV并转换为时间序列DataFrame

  4. 支持按日期范围过滤数据

# 2. 从网络获取symbol = f'sh{etf_code}' if etf_code.startswith('51') else f'sz{etf_code}'print(f"🌐 从网络获取: {etf_code}")df = ak.fund_etf_hist_sina(symbol=symbol)

网络获取逻辑

  1. 代码转换:根据ETF代码前缀(51或159/588)确定市场标识符

  • 调用akshare:使用fund_etf_hist_sina函数获取新浪财经数据

  • 数据格式化:重命名列、设置日期索引、类型转换

  • 数据存储管理

    # 创建数据目录data_dir = "行情数据库"os.makedirs(data_dir, exist_ok=True)# 保存数据df.to_csv(filepath, encoding='utf-8-sig', index=True)

    存储策略

    • 目录结构:所有数据文件集中存放在"行情数据库"目录

    • 文件命名{ETF名称}_{ETF代码}.csv,便于搜索和识别

    • 编码格式utf-8-sig支持中文且兼容Excel

    • 索引保存:保存索引(日期),便于后续读取

    状态更新与反馈

    # 更新表格行数据updated_data.append({    'etf_name': etf_name,    'etf_code': etf_code,    'latest_date': datetime.now().strftime('%Y-%m-%d'),    'kline_count': f'{rows:,}',  # 添加千位分隔符    'status': status,  # ✅ 已保存 / ❌ 无数据 / ❌ 错误    'action': '📊 详情'})# 更新按钮文字button_text = f"✅ 完成 {success_count}/{total_etfs} ({datetime.now().strftime('%H:%M:%S')})"

    用户反馈机制

    1. 表格状态列:使用表情符号直观显示状态

    • ✅ 成功状态

    • ⚠️ 警告状态

    • ❌ 错误状态

  • 按钮动态文字:显示完成进度和时间戳

  • 控制台日志:详细记录每个ETF的处理过程

  • 错误处理机制

    except Exception as e:    status = '❌ 错误'    rows = 0    error_msg = str(e)[:50]    print(f"    ✗ 异常: {error_msg}")

    多层错误处理

    1. 单个ETF异常处理:不会中断整个更新流程

    2. 主流程异常处理:保证返回有效数据

    3. 最终安全网:返回默认数据结构

    4. 详细日志记录:控制台输出错误信息,便于调试

    # 即使出错也要返回有效数据error_data = current_table_data if current_table_data else []error_msg = f"❌ 错误: {str(e)[:30]}"return error_data, error_msg

    数据流完整流程总结

    用户点击"立即更新"按钮↓触发update_etf_data()回调函数↓遍历IDX_SYM中的所有ETF    ↓    对于每个ETF:        1. 检查本地数据 → 如果存在且完整,直接使用        2. 本地无数据 → 调用get_etf_data()            ↓            a. 构建akshare查询参数            b. 从新浪财经获取数据            c. 数据清洗和格式化        3. 保存数据到本地CSV文件↓更新表格显示状态↓更新按钮文字为完成状态↓用户看到更新成功的反馈

    异常流程(失败情况):

    网络故障或数据源不可用↓get_etf_data()返回None或空DataFrame↓update_etf_data()捕获异常↓标记该ETF为错误状态↓继续处理下一个ETF(不中断整个流程)↓最终返回部分成功的结果↓用户看到哪些成功、哪些失败

    数据保存结构:

    行情数据库/├── 上证50ETF_510050.csv├── 沪深300ETF_159919.csv├── 创业板指_159915.csv├── 证券ETF_512880.csv└── ...(其他ETF)
    集成backtrader

    整体架构:前端采用Dash构建响应式Web界面,通过Bootstrap进行现代化UI设计;后端核心是Backtrader量化回测引擎,通过回调函数与前端交互;数据层使用AKShare从新浪财经获取实时ETF历史数据,形成三层架构设计。

    数据流:用户在Web界面设置参数 → 点击回测按钮触发Dash回调 → Backtrader引擎加载ETF历史数据 → 执行动量轮动策略 → 生成交易记录和绩效指标 → 结果返回前端可视化展示。

    技术栈:Dash(前端框架)+ Backtrader(量化回测)+ AKShare(数据获取)+ Pandas(数据处理)+ Plotly(数据可视化),形成一个完整的量化策略研发工具链。

    Backtrader策略类 (MomentumRotationStrategy)

    class MomentumRotationStrategy(bt.Strategy):    """    动量轮动策略的核心实现    继承自Backtrader的bt.Strategy基类    """    params = (        ('momentum_period', 5),     # 动量计算周期        ('rebalance_period', 5),    # 调仓周期        ('hold_num', 2),           # 持有标的数量        ('min_momentum', 0),       # 最小动量阈值        ('position_size', 0.4),    # 单个标的仓位比例        ('printlog', True),        # 日志开关    )
    参数说明:
    • momentum_period: 计算动量(收益率)的回顾周期

    • rebalance_period: 调仓间隔天数

    • hold_num: 每次持有表现最好的ETF数量

    • min_momentum: 动量过滤阈值,避免买入弱势标的

    • position_size: 每个ETF的最大仓位比例

    接下来介绍下类中的各个方法。

    __init__() - 初始化方法
    • 使用Backtrader内置的Momentum指标计算器

    • self.datas包含所有添加到回测的ETF数据

    • 使用字典管理动量值,便于快速访问

    def __init__(self):    # 为每个ETF计算动量指标    self.momentum = {        data: bt.indicators.Momentum(data.close, period=self.p.momentum_period)         for data in self.datas    }    # 存储ETF排序结果    self.rankings = list(self.datas)    # 调仓计数器    self.rebalance_day = 0    # 订单字典,管理未完成订单    self.order_dict = {}
    next() - 逐日执行方法
    执行逻辑:
    • 每个交易日调用一次

    • 计数器累加,达到调仓周期时触发调仓

    • 重置计数器

    def next(self):    self.rebalance_day += 1    # 到达调仓日时执行调仓    if self.rebalance_day >= self.p.rebalance_period:        self.rebalance_portfolio()        self.rebalance_day = 0
    rebalance_portfolio() - 核心调仓逻辑
    调仓算法:
    • 排序阶段:计算所有ETF的动量并排序

    • 卖出阶段:检查当前持仓,如果不在前N名则卖出

    • 买入阶段:买入排名前N且动量达标的ETF

    • 仓位控制:按比例分配资金,按手数取整

    def rebalance_portfolio(self):    """执行调仓:卖出不在前列的,买入排名前列的"""    # 1. 按动量值排序(从高到低)    self.rankings.sort(key=lambda d: self.momentum[d][0], reverse=True)    # 2. 卖出不在前hold_num名的持仓    for data in self.datas:        pos = self.getposition(data).size        if pos > 0 and data not in self.rankings[:self.p.hold_num]:            self.close(data=data)  # 平仓    # 3. 买入排名前hold_num且动量达标的标的    for i in range(min(self.p.hold_num, len(self.rankings))):        data = self.rankings[i]        pos = self.getposition(data).size        momentum_value = self.momentum[data][0]        if pos == 0 and momentum_value > self.p.min_momentum and data not in self.order_dict:            # 计算买入数量            target_value = self.broker.getvalue() * self.p.position_size            size = int(target_value / data.close[0] / 100) * 100  # 按手数取整            if size > 0:                self.order_dict[data] = self.buy(data=data, size=size)
    notify_order() - 订单状态回调
    作用:
    • 记录交易日志

    • 清理已完成订单

    • 处理异常订单状态

    def notify_order(self, order):    """处理订单状态变化"""    if order.status in [order.Completed]:        if order.isbuy():            self.log(f'买入执行 {order.data._name}, 价格:{order.executed.price:.2f}')        elif order.issell():            self.log(f'卖出执行 {order.data._name}, 价格:{order.executed.price:.2f}')        self.order_dict.pop(order.data, None)  # 移除已完成的订单
    ETF数据获取函数
    当用户点击"开始回测"按钮时,优先尝试从本地的"行情数据库"目录读取CSV文件,如果本地没有相应数据或读取失败,则自动从网络(新浪财经)实时获取最新数据。
    def get_etf_data(etf_code, start_date=None, end_date=None, use_local=True):    """使用akshare获取ETF数据 - 纯读取,不保存"""    try:        # 1. 优先读取本地(如果启用)        if use_local:            try:                data_dir = "行情数据库"                for filename in os.listdir(data_dir):                    if filename.endswith('.csv') and (                            filename == f"{etf_code}.csv" or f"_{etf_code}.csv" in filename):                        filepath = os.path.join(data_dir, filename)                        df = pd.read_csv(filepath, index_col='date', parse_dates=True)                        if 'open' in df.columns:                            # 按日期过滤                            if start_date and end_date:                                start_dt = pd.to_datetime(start_date)                                end_dt = pd.to_datetime(end_date)                                df = df[(df.index >= start_dt) & (df.index <= end_dt)]                            if not df.empty:                                print(f"📁 从本地读取: {len(df)} 行")                                return df            except:                pass  # 本地读取失败,静默继续        # 2. 从网络获取        symbol = f'sh{etf_code}' if etf_code.startswith('51') else f'sz{etf_code}'        print(f"🌐 从网络获取: {etf_code}")        df = ak.fund_etf_hist_sina(symbol=symbol)        if df.empty:            print(f"数据为空")            return None        # 数据处理        df = df.rename(columns={'date': 'date', 'open': 'open', 'high': 'high',                                'low': 'low', 'close': 'close', 'volume': 'volume'})        df['date'] = pd.to_datetime(df['date'])        df.set_index('date', inplace=True)        df.sort_index(inplace=True)        # 日期过滤        if start_date and end_date:            start_dt = pd.to_datetime(start_date)            end_dt = pd.to_datetime(end_date)            df = df[(df.index >= start_dt) & (df.index <= end_dt)]        # 数据类型转换        for col in ['open', 'high', 'low', 'close', 'volume']:            df[col] = pd.to_numeric(df[col], errors='coerce')        df = df.dropna()        print(f"  成功: {len(df)} 行数据")        return df    except Exception as e:        print(f"获取 {etf_code} 失败: {e}")        return None

    数据格式转换

    # Backtrader需要PandasData格式data = bt.feeds.PandasData(    dataname=df,           # 原始DataFrame    name=etf_code,         # 数据名称(用于识别)    datetime=None,         # 使用索引作为日期    open=0, high=1,        # 列索引映射    low=2, close=3,     volume=4,              # 成交量列    openinterest=-1        # 无持仓量数据)

    回测引擎配置与执行。Cerebro引擎初始化

    def run_backtest(start_date, end_date, initial_capital, params):    # 创建回测引擎    cerebro = bt.Cerebro()    # 设置初始资金    cerebro.broker.setcash(initial_capital)    # 设置交易费用(万三)    cerebro.broker.setcommission(commission=0.0003)    # 添加数据    for etf_code in etf_pool:        df = get_etf_data(etf_code, start_date, end_date)        if df is not None and len(df) > 50:            data = bt.feeds.PandasData(                dataname=df,                name=etf_code,                datetime=None,                open=0, high=1, low=2, close=3, volume=4,                openinterest=-1            )            cerebro.adddata(data)    # 添加策略    cerebro.addstrategy(        MomentumRotationStrategy,        momentum_period=params['momentum_period'],        rebalance_period=params['rebalance_period'],        hold_num=params['hold_num'],        position_size=params['position_size'],        printlog=False    )    # 添加分析器    cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='sharpe', riskfreerate=0.0)    cerebro.addanalyzer(bt.analyzers.DrawDown, _name='drawdown')    cerebro.addanalyzer(bt.analyzers.Returns, _name='returns')    cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name='trades')    cerebro.addanalyzer(bt.analyzers.TimeReturn, _name='timereturn')    # 运行回测    results = cerebro.run()    strat = results[0]    return cerebro, strat

    Dash回调函数集成。回测触发回调

    @app.callback(    [Output('total-return-display', 'children'),     Output('sharpe-display', 'children'),     Output('max-drawdown-display', 'children'),     Output('win-rate-display', 'children'),     Output('total-trades-display', 'children'),     Output('profit-factor-display', 'children'),     Output('equity-curve-graph', 'figure'),     Output('backtest-status', 'children')],    [Input('start-backtest-btn', 'n_clicks')],    [State('backtest-date-range', 'start_date'),     State('backtest-date-range', 'end_date'),     State('initial-capital-input', 'value'),     State('rebalance-period-select', 'value'),     State('hold-num-input', 'value'),     State('momentum-period-input', 'value')])defrun_real_backtest(n_clicks, start_date, end_date, initial_capital,                      rebalance_period, hold_num, momentum_period):    """    完整的回测流程:    1. 获取参数    2. 获取数据    3. 运行Backtrader回测    4. 提取结果    5. 生成可视化    """    # 参数验证    if n_clicks isNone:        return ['-'] * 7 + ['等待开始回测...']    try:        # 1. 准备ETF池        etf_pool = list(IDX_SYM.values())
            # 2. 初始化Backtrader引擎        cerebro = bt.Cerebro()        cerebro.broker.setcash(initial_capital)        cerebro.broker.setcommission(commission=0.0003)
            # 3. 批量获取数据        data_frames = {}        for etf_code in etf_pool:            try:                df = get_etf_data(etf_code, start_date, end_date)                if df isnotNoneandnot df.empty andlen(df) > 50:                    data_frames[etf_code] = df            except Exception as e:                continue
            # 4. 添加数据到引擎        for etf_code, df in data_frames.items():            data = bt.feeds.PandasData(                dataname=df,                name=etf_code,                datetime=None,                open=0, high=1, low=2, close=3, volume=4,                openinterest=-1            )            cerebro.adddata(data)
            # 5. 添加策略        cerebro.addstrategy(            MomentumRotationStrategy,            momentum_period=momentum_period,            rebalance_period=rebalance_period,            hold_num=hold_num,            position_size=0.4,            printlog=False        )
            # 6. 添加分析器        cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='sharpe', riskfreerate=0.0)        cerebro.addanalyzer(bt.analyzers.DrawDown, _name='drawdown')        cerebro.addanalyzer(bt.analyzers.Returns, _name='returns', timeframe=bt.TimeFrame.Days)        cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name='trades')        cerebro.addanalyzer(bt.analyzers.TimeReturn, _name='timereturn')
            # 7. 运行回测        results = cerebro.run()        strat = results[0]
            # 8. 提取绩效指标        final_value = cerebro.broker.getvalue()        total_return = (final_value / initial_capital - 1) * 100
            # 获取夏普比率        sharpe_analysis = strat.analyzers.sharpe.get_analysis()        sharpe_ratio = sharpe_analysis.get('sharperatio', 0)
            # 获取最大回撤        drawdown_analysis = strat.analyzers.drawdown.get_analysis()        max_drawdown = drawdown_analysis.get('max', {}).get('drawdown', 0)
            # 获取交易分析        trade_analysis = strat.analyzers.trades.get_analysis()        total_trades = trade_analysis.get('total', {}).get('closed', 0) ifhasattr(trade_analysis, 'total') else0
            # 9. 生成净值曲线        time_return = strat.analyzers.timereturn.get_analysis()        dates = []        values = []        current_value = initial_capital
            for date_key insorted(time_return.keys()):            daily_return = time_return[date_key]            current_value *= (1 + daily_return)            dates.append(date_key)            values.append(current_value)
            # 10. 创建图表        equity_fig = {            'data': [{                'x': dates,                'y': values,                'type': 'line',                'name': '策略净值',                'line': {'color': '#3498db', 'width': 2}            }],            'layout': {                'title': f'动量轮动策略净值曲线',                'xaxis': {'title': '日期'},                'yaxis': {'title': '净值'},                'height': 400            }        }
            # 11. 格式化返回结果        return [            f"{total_return:+.2f}%",          # 总收益率            f"{sharpe_ratio:.2f}",           # 夏普比率            f"-{max_drawdown:.2f}%",         # 最大回撤            f"{win_rate:.2f}%"if total_trades > 0else"-",  # 胜率            f"{total_trades}",               # 交易次数            f"{profit_factor:.2f}"if total_trades > 0else"-",  # 盈亏比            equity_fig,                      # 净值曲线图            f"回测完成 | 总收益: {total_return:+.2f}%"  # 状态信息        ]
        except Exception as e:        return ['-'] * 6 + [empty_fig, f"❌ 回测失败: {str(e)}"]
    以上的设计实现了从数据获取(AKShare实时行情)、策略执行(动量轮动算法)到绩效分析(夏普比率、最大回撤等指标)的完整闭环。
    执行日志如下所示:
    ============================================================2024-04-23 | 📅 调仓日期: 2024-04-232024-04-23 | 💰 当前总资产: 1000000.002024-04-23 | 📊 ETF动量排名 (周期=20天):2024-04-23 |    1. 159562     动量: +0.09 2024-04-23 |    2. 159652     动量: +0.04 2024-04-23 |    3. 513230     动量: +0.03 2024-04-23 |    4. 513530     动量: +0.02 2024-04-23 |    5. 159870     动量: +0.01 2024-04-23 |    6. 510050     动量: +0.01 2024-04-23 |    7. 159825     动量: +0.00 2024-04-23 |    8. 159750     动量: -0.01 2024-04-23 |    9. 512690     动量: -0.01 2024-04-23 |   10. 159840     动量: -0.02 2024-04-23 | 📥 买入清单 (2只):2024-04-23 |   ✔ 159562: 312000股 (动量: +0.09)2024-04-23 |   ✔ 159652: 447900股 (动量: +0.04)2024-04-23 | 📈 最终持仓 (0只):2024-04-23 | ✅ 调仓完成2024-04-23 | ============================================================2024-04-24 | 💰 买入执行 #001 159562, 价格:1.282, 数量:312000, 金额:399984.002024-04-24 | 💰 买入执行 #002 159652, 价格:0.879, 数量:447900, 金额:393704.102024-05-24 | ============================================================2024-05-24 | 📅 调仓日期: 2024-05-242024-05-24 | 💰 当前总资产: 1044997.992024-05-24 | 📊 ETF动量排名 (周期=20天):2024-05-24 |    1. 513530     动量: +0.20 2024-05-24 |    2. 159509     动量: +0.15 2024-05-24 |    3. 513090     动量: +0.11 2024-05-24 |    4. 159919     动量: +0.11 2024-05-24 |    5. 159902     动量: +0.10 2024-05-24 |    6. 159562     动量: +0.09 持有2024-05-24 |    7. 512200     动量: +0.09 2024-05-24 |    8. 159915     动量: +0.08 2024-05-24 |    9. 513290     动量: +0.07 2024-05-24 |   10. 510050     动量: +0.06 2024-05-24 | 📤 卖出清单 (2只):2024-05-24 |   ✖ 159562: 312000股 (不在前2名)2024-05-24 |   ✖ 159652: 447900股 (不在前2名)2024-05-24 | 📥 买入清单 (2只):2024-05-24 |   ✔ 513530: 306900股 (动量: +0.20)2024-05-24 |   ✔ 159509: 296400股 (动量: +0.15)2024-05-24 | 📈 最终持仓 (2只):2024-05-24 |   📌 159562: 312000股2024-05-24 |   📌 159652: 447900股2024-05-24 | ✅ 调仓完成2024-05-24 | ============================================================2024-05-27 | 💰 卖出执行 #003 159562, 价格:1.371, 数量:-312000, 金额:-427752.00, 盈利:27768.002024-05-27 | 💰 卖出执行 #004 159652, 价格:0.914, 数量:-447900, 金额:-409380.60, 盈利:15676.502024-05-27 | 💰 买入执行 #005 513530, 价格:1.366, 数量:306900, 金额:419225.402024-05-27 | 💰 买入执行 #006 159509, 价格:1.451, 数量:296400, 金额:430076.402024-06-24 |
    为什么用Dash
    在量化交易领域,我们经常面临一个尴尬的局面:策略模型很强大,回测结果很漂亮,但展示给他人时却只能拿出冰冷的Excel表格或简单的Matplotlib图表。传统的Web开发需要HTML、CSS、JavaScript等前端技术,这对很多量化分析师和数据科学家来说是个不小的门槛。
    直到我发现了Dash——这个基于Python的Web应用框架,让我能够用纯Python代码构建出专业级的交互式Web应用。

    Dash是Plotly公司开源的Python框架,专门用于构建分析型Web应用。它的核心优势在于:

    • 纯Python开发:无需编写任何HTML/CSS/JavaScript

    • 响应式设计:自动适配桌面和移动端

    • 丰富的组件:内置图表、表格、下拉菜单等交互组件

    • 实时更新:支持数据实时刷新和交互

    • 企业级应用:支持多用户、权限控制等高级功能

    以下是Dash 最简完整例程说明

    # demo.py - 最简单的Dash应用import dashfrom dash import dcc, html, Input, Output# 1️⃣ 创建应用对象(必须)app = dash.Dash(__name__)# 2️⃣ 定义布局:网页上显示什么app.layout = html.Div([    # A. 标题(用html.H1创建大标题)    html.H1("最简单的Dash应用", id='main-title'),    # B. 文本输入框(用户在这里输入内容)    dcc.Input(        id='user-input',          # 组件ID,回调函数用这个找组件        type='text',              # 输入类型        value='请输入文字',       # 默认显示的文字        style={'width': '300px'}  # CSS样式:宽度300像素    ),    # C. 显示区域(用来显示处理后的结果)    html.Div(id='output-div', style={'marginTop': '20px'}),    # D. 按钮(用户点击触发动作)    html.Button('清空输入', id='clear-btn')])# 3️⃣ 定义回调:用户操作后系统怎么响应@app.callback(    # 输出:更新哪个组件的哪个属性    Output('output-div', 'children'),    # 输入:监听哪个组件的哪个属性变化    [        Input('user-input', 'value'),    # 监听输入框内容变化        Input('clear-btn', 'n_clicks')   # 监听按钮点击次数    ])def update_output(input_text, click_count):    """    回调函数:当输入框内容变化或按钮被点击时自动执行    参数:    input_text: 输入框当前的内容    click_count: 按钮被点击的次数    返回:    要显示在output-div中的内容    """    # 判断触发回调的是哪个组件    ctx = dash.callback_context    if not ctx.triggered:        # 第一次加载页面时执行        return "等待输入..."    else:        # 获取是哪个输入触发的        trigger_id = ctx.triggered[0]['prop_id'].split('.')[0]        if trigger_id == 'clear-btn':            # 如果点击了清空按钮            return "输入已清空!"        else:            # 如果输入框内容变化了            return f"你输入了:{input_text}"# 4️⃣ 运行应用(启动服务器)if __name__ == '__main__':    # 打印访问地址    print("✅ 应用已启动!")    print("🌐 请用浏览器访问:http://localhost:8050")    print("📝 在输入框中打字,下面会实时显示")    print("🔄 点击按钮可以清空内容")    # 启动服务器    app.run_server(        debug=True,      # 调试模式,代码修改会自动重启        port=8050        # 端口号    )
    在Dash框架中,布局(Layout) 定义了网页的整体结构与外观,即页面长什么样;它由一个个组件(Component) 构成,这些组件就是网页上的各个可视部分,如输入框、按钮、图表等。回调(Callback) 则负责编写交互逻辑,它通过监听组件状态的变化来触发相应的处理函数。而响应(Response) 描述了事件触发后的处理流程,即系统如何响应具体的用户操作或数据变化,从而动态更新页面内容。
    在这个最简例程基础上,可以添加图表、页面和接入真实数据
    添加图表:
    # 添加图表组件dcc.Graph(id='my-chart')# 在回调中返回图表return dcc.Graph(figure=create_chart(data))
    添加更多页面
    # 用回调切换不同内容@app.callback(    Output('content', 'children'),    Input('tab1', 'n_clicks'),    Input('tab2', 'n_clicks'))def switch_page(click1, click2):    # 返回不同页面的内容
    连接真实数据
    # 读取CSV文件import pandas as pddf = pd.read_csv('data.csv')# 在回调中使用数据def update_chart(selection):    filtered = df[df['category'] == selection]    return create_chart(filtered)
    关键技术实现
    整体布局结构:系统采用经典的侧边栏导航+主内容区设计,左侧为功能导航菜单,右侧为具体内容展示区:
    app.layout = html.Div([    # 标题栏    html.Div([...]),    # 主内容区域    html.Div([        # 侧边栏导航(固定250px)        html.Div([...], id="sidebar"),        # 主内容展示区        html.Div([...], id="main-content")    ])])

    响应式导航系统:通过Dash的回调机制,实现页面间的平滑切换:

    @app.callback(    [Output("content-pool", "style"),     Output("content-account", "style"),     ...],    [Input("nav-pool", "n_clicks"),     Input("nav-account", "n_clicks"),     ...])def switch_page(pool_clicks, account_clicks, ...):    # 动态显示/隐藏页面,更新导航状态    ...

    部署与运行

    # 安装依赖pip install dash plotly pandas numpy akshare# 运行应用python app.py# 生产环境部署(使用Gunicorn)gunicorn -w 4 -b 0.0.0.0:8050 app:server

    应用启动后,在浏览器中访问 http://localhost:8050 即可使用。

    总结

    Dash真正实现了"用Python解决所有问题"的理念,让数据科学家和量化分析师能够专注于核心业务逻辑,而不是前端技术细节。

    在这套动量轮动策略的Web界面框架中,我们采用了界面原型与功能框架先行的开发策略。当前版本已完成完整的交互界面布局和各功能模块的逻辑架构,其中展示数据采用了模拟数据与真实数据结构相结合的方式。

    后续所有数据接口、回调函数和计算模块均采用标准化设计,使用者只需将现有模拟数据源替换为真实的行情API,并将策略逻辑的核心计算函数替换为实盘算法,即可在不改动任何界面代码的情况下,快速将演示系统升级为完整的生产级交易平台。

    说明

    此系列为连载专栏,完整代码会上传知识星球《Python量化编程基础》!作为《玩转股票量化交易》学员们的学习资料。非《玩转股票量化交易》星球学员需要的话,也可以联系我加入!

    知识星球介绍点击:知识星球《玩转股票量化交易》精华内容概览

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值