
升级说明

上一篇,我们把backtrader回测框架集成到系统里面,实现了真实的历史数据回测轮动策略。
[升级]21天搭建ETF量化交易系统Day19—Web版轮动系统,深度集成Backtrader回测!
本次升级,本地数据管理模块!实现一键更新行情数据到本地缓存,后续回测直接调用本地数据,大幅提升策略回测速度和稳定性。

搭建过程

这个系列,我们用Python从0一步步搭建出一套ETF量化交易系统(选择ETF标的是因为对于普通交易者来说,ETF相对于选强势股难度要小,而且没有退市风险)。大家可以跟随着我们的实现路径来一起学习,从过程中掌握方法。
掌握了方法之后,可以换成期货系统、比特币系统、美股系统,然后在实战中不断去完善自己的系统了。
DAY1-DAY21链接如下:21天搭建ETF量化交易系统
我们与时俱进,还会持续对现有的DAY21的内容升级!
数据管理模块实现当前数据流架构:用户点击"立即更新" → `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)框架功能展示我的轮动池:提供ETF池管理,支持添加、筛选、排序ETF以构建个性化轮动池;实现动量排名可视化,能够实时计算并展示各ETF的动量分数及排名变化;配备一键式操作界面,集成导入/导出池子、批量操作等便捷功能;并通过多维度数据展示,将价格、涨跌幅、动量分数、排名等关键指标清晰呈现,实现策略状态一目了然。动量轮动策略核心逻辑: 每20天定期调仓 选择动量排名前2的ETF持有 动量分数基于20日涨幅计算 实现"强者恒强"的轮动效应 我的账户:提供了全方位的资产全景视角:通过总资产、持仓市值、可用资金、累计收益四大关键指标呈现核心财务状况;以交互式资产配置饼图直观展示各ETF持仓比例,并支持点击查看详情;同时配备动态收益曲线,实时追踪账户净值变化以评估策略表现;并清晰展示当前仓位比例,为投资决策提供直接的仓位管理参考。策略回测:构建参数化回测系统,支持灵活设置回测周期、初始资金及调仓频率等核心参数。系统通过绩效指标矩阵以卡片化形式展示12项关键指标,对策略表现进行全面评估;同时提供净值对比分析功能,将策略净值与基准净值进行多时间维度的可视化比较。所有分析均以交互式图表呈现,支持缩放、平移及数据点悬停查看,使历史验证过程直观而深入。参数配置:策略参数涵盖动量周期、调仓频率及持有数量等核心策略变量;交易参数包括手续费率、滑点及交易时间等执行细节;风险参数则聚焦于止损止盈、仓位限制与风险控制等风控规则,形成从策略逻辑、交易执行到风险管控的完整闭环。实盘明细:提供交易全流程监控。支持按时间、交易类型及ETF代码等多维度精准筛选交易记录;采用清晰的状态标记体系,有效区分买入/卖出、盈利/亏损以及完成/待处理等关键状态;完整记录每笔交易的时间、价格、数量及金额,实现交易详情可追溯;同时自动计算并展示每笔交易的盈亏情况,为绩效归因提供精确数据支撑。数据管理:实现一体化数据管理。支持每日、每周及每月定时更新的自动配置计划;集成新浪财经、东方财富、聚宽等多类主流数据源;实时监控各类数据的更新状态,并通过清晰的状态标识进行可视化反馈;同时提供手动控制接口,允许用户按需触发数据的立即更新,保障数据时效性与系统灵活性。 系统监控:提供全方位的稳定性保障。通过服务状态看板实时展示服务器、数据库、策略引擎及交易接口四大核心组件的运行状态;以动态曲线图监控CPU与内存等资源的使用率;按时间倒序追踪并展示关键操作日志,确保运行过程可审计;同时建立异常预警机制,实现对系统异常的即时发现与快速处理,有效保障系统持续稳定运行。策略文档:构建了完善的知识沉淀体系。详细阐述动量轮动策略的理论基础与核心原理;提供从入门到精通的完整操作使用指南;分享基于实际系统运行总结的最佳实践经验;并设立明确的技术支持通道,确保用户在遇到问题时能通过指定联系方式获得快速响应与解决方案。 关键技术实现 整体布局结构:系统采用经典的侧边栏导航+主内容区设计,左侧为功能导航菜单,右侧为具体内容展示区: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量化编程基础》!作为《玩转股票量化交易》学员们的学习资料。非《玩转股票量化交易》星球学员需要的话,也可以联系我加入!
知识星球介绍点击:知识星球《玩转股票量化交易》精华内容概览

5345

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



