
升级说明

回顾下前几次升级的内容:
我们把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多进程架构基础上,新增实盘委托查询与展示功能。用户可通过“实盘明细”页面实时查询当日委托记录,完整了解每笔交易的状态和详情。委托明细实盘接入整体架构
用户点击导航菜单“实盘明细”
页面加载,显示空白表格和刷新按钮
用户点击“🔄 刷新”按钮
Dash回调触发,通过QMT客户端发送
get_orders命令QMT工作进程调用
trader.query_stock_orders()获取委托数据数据经解析后返回主进程
表格自动更新,显示当日委托记录
在
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 NoneETF表格生成回调(核心实现)
@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的实时同步:
主进程:Dash Web应用,负责UI展示
子进程:QMT工作进程,负责与QMT交易系统通信
进程间通信:通过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中。
下单模块实现下单模块是动量策略轮动系统的核心交易执行组件,负责处理从用户界面发起的所有交易指令,与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)
核心步骤:
调用
get_etf_data(etf_code):获取该ETF所有可用历史数据
本地存储:文件名格式:{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)
本地读取逻辑:
遍历"行情数据库"目录下所有CSV文件
通过文件名匹配(包含ETF代码)
读取CSV并转换为时间序列DataFrame
支持按日期范围过滤数据
# 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)
网络获取逻辑:
代码转换:根据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')})"用户反馈机制:
表格状态列:使用表情符号直观显示状态
✅ 成功状态
⚠️ 警告状态
❌ 错误状态
按钮动态文字:显示完成进度和时间戳
控制台日志:详细记录每个ETF的处理过程
错误处理机制
except Exception as e: status = '❌ 错误' rows = 0 error_msg = str(e)[:50] print(f" ✗ 异常: {error_msg}")多层错误处理:
单个ETF异常处理:不会中断整个更新流程
主流程异常处理:保证返回有效数据
最终安全网:返回默认数据结构
详细日志记录:控制台输出错误信息,便于调试
# 即使出错也要返回有效数据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 = 0rebalance_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) # 移除已完成的订单
当用户点击"开始回测"按钮时,优先尝试从本地的"行情数据库"目录读取CSV文件,如果本地没有相应数据或读取失败,则自动从网络(新浪财经)实时获取最新数据。ETF数据获取函数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, stratDash回调函数集成。回测触发回调
@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量化编程基础》!作为《玩转股票量化交易》学员们的学习资料。非《玩转股票量化交易》星球学员需要的话,也可以联系我加入!
知识星球介绍点击:知识星球《玩转股票量化交易》精华内容概览





1120

被折叠的 条评论
为什么被折叠?



