ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Python期货量化交易系统实战:从数据清洗到策略回测与模拟盘验证

2026/10/2 9:06:15 拓冰建站 浏览量
Python期货量化交易系统实战:从数据清洗到策略回测与模拟盘验证 简介Python期货量化交易系统是一套面向金融科技学习者与高校学生的完整项目资源融合深度学习与人工智能算法实现期货行情分析与自动化交易决策适合作为毕业设计或课程设计选题。压缩包共120个文件主体为71个Python脚本承担数据服务、策略回测与接口封装另有C头文件与动态库用于对接期货交易API以及配置、说明和安全文档包体仅6.89MB结构紧凑便于本地部署。系统内置数据服务启停脚本与核心执行模块封装交易策略、市场分析和订单执行逻辑同时附带安装流程、安全策略和版本控制等说明可支撑从环境配置到策略回测的完整实践链路。已有180人学习下载对研究机器学习在量化交易中落地的开发者而言这是一个可直接运行并二次扩展的高价值案例尤其适合作为金融科技方向的实战练习。1. Python期货量化交易系统一套能让你跑通全流程的源码包我在期货公司蹲过两年见过太多手工交易员被来回打脸日内盯盘盯出腱鞘炎止损单没来得及敲进去行情瞬间反向账户直接趴窝。后来转战量化最深的体会是——期货这玩意儿T0、多空双向、杠杆放大的特性天生就适合用程序去捕捉规则而不是靠人脑去追涨杀跌。这套Python期货量化交易系统zip资源不是一个写着玩的毕业设计空壳它把数据获取、策略回测、模拟盘执行这几段最常卡住新手的环节全部串起来了你拿到手能跑通数据管线能改策略参数能看到回测曲线也能接上模拟账户去验证实盘逻辑。尤其适合那些懂点Python基础、想入门期货量化又被各种坑劝退的人或者正在做课程设计、人工智能方向作业的同学——深度学习那层算锦上添花核心是让你从零到一建立一套可用的交易系统框架。2. 数据层用免费接口拉期货行情先把主力连续和复权搞明白2.1 数据源选型tushare和akshare的边界做量化第一步是拿数据这一步90%的人翻车。不要一上来就追求什么Tick级数据、十档盘口你没那个资金量也没必要。大多数策略在日线和分钟线上就能验证所以免费接口完全够用。我常用的是tushare的Pro接口和akshare。前者需要你注册拿token基础积分能取日线分钟线要么积分不够要么限流后者不需要token接口多且杂取数据有时候像开盲盒但胜在零门槛。从这套系统的角度来说建议把akshara作为主数据源因为它的期货历史数据覆盖连续合约还内置了复权因子省掉你自己拼主力合约的活。但注意akshare部分接口偶尔会改字段名你得在代码里做好异常兜底别假设今天能跑明天还能跑。2.2 主力合约连续与复权为什么你的K线会跳空期货和股票最大的不同是换月主力合约持仓量变化后交易标的会从1月合约跳到5月合约两个合约的价格可能差了几十点。如果不处理你的均线在金叉之前就会被这个跳空直接打断。复权有两种前复权和后复权。对量化回测我推荐用后复权——它保持历史价格不变按换月时的价差调整后续价格这样不会引入未来信息。下面这段代码展示如何用akshare拉取螺纹钢主力连续的后复权日线并做基本清洗import akshare as ak import pandas as pd # 拉取螺纹钢主力连续日线数据adjustqfq是前复权hfq是后复权 df ak.futures_main_sina(symbolRB0, start_date20200101, end_date20241201, adjusthfq) # 统一列名原始列名可能是中文必须重命名 df.rename(columns{ 日期: datetime, 开盘价: open, 最高价: high, 最低价: low, 收盘价: close, 成交量: volume, 持仓量: open_interest }, inplaceTrue) # 去除停牌或空数据行 df.dropna(subset[open, close, volume], inplaceTrue) # 排序并重置索引确保按时间升序 df.sort_values(datetime, inplaceTrue) df.reset_index(dropTrue, inplaceTrue) # 只保留标准的OHLCV字段日期转成datetime格式 df[datetime] pd.to_datetime(df[datetime]) df df[[datetime, open, high, low, close, volume, open_interest]] print(df.head())这段代码干了三件事拉取数据、重命名、清洗。重点在adjusthfq这个参数如果你写qfq后续策略回测时历史价格会被换月后的新价格调整相当于把未来信息带进了过去回测结果虚高。symbolRB0是螺纹钢主力连续的代码每个品种不一样豆粕是M0原油是SC0你得去查对应代码表。start_date和end_date控制时间范围注意akshare这个接口日期是字符串格式不能错错了直接空返回。2.3 统一存储格式别用CSV用parquet很多人的习惯是df.to_csv()一存了事。但期货全品种多年的日线加分钟线CSV不仅占空间大而且每读一次都要重新解析回测跑起来慢得让你怀疑人生。我更推荐用parquet格式列式存储读取快还保留数据类型。下面是存储和读取的封装import pyarrow.parquet as pq import pyarrow as pa def save_to_parquet(df, path): 保存DataFrame到parquet文件避免CSV反复解析 table pa.Table.from_pandas(df) pq.write_table(table, path, compressionsnappy) print(f数据已保存至 {path}) def load_from_parquet(path): 从parquet文件加载DataFrame table pq.read_table(path) df table.to_pandas() return df # 示例把螺纹钢数据保存然后重新读取 save_to_parquet(df, data/RB0_daily.parquet) loaded_df load_from_parquet(data/RB0_daily.parquet) print(loaded_df.shape)参数上compressionsnappy是个平衡点压缩率不是最高但解压速度快适合多次回测读取。文件路径建议按品种_周期的规则组织比如RB0_daily.parquet、M0_15min.parquet这样后续加品种不用重建目录结构。还需要说明的是open_interest持仓量在期货里是有用的调仓信号不要学股票量化把它扔了。3. 策略层双均线打底加ATR和资金管理才敢实盘3.1 为什么拿双均线开局很多新手一上来就问有没有深度学习模型直接预测涨跌我给你的回答是别闹。深度学习在期货上不是不能用但那需要海量数据、特征工程和严谨的验证流程不是你在Kaggle上跑个LSTM就能赚钱的。一套靠谱的期货策略起步必须逻辑透明、参数可解释。双均线正好满足快线上穿慢线开多下穿开空就这么简单。它解决的是趋势跟随问题期货又是双向T0趋势策略天然有优势所以它即便在2020年以后也依然有效——不是因为它能预测价格而是因为它能保证你跟到大趋势。但裸双均线有两个致命弱点震荡行情里来回打脸以及满仓进出导致回撤巨大。加上ATR过滤器和固定比例仓位管理就能把这两个问题摁住。3.2 完整策略代码信号生成与仓位计算核心逻辑分三步计算均线和ATR用ATR过滤掉窄幅震荡然后再算目标仓位。代码里我直接把信号和仓位算在一起方便你直接拿去回测import numpy as np import pandas as pd def add_signal_and_position(df, fast5, slow20, atr_period14, atr_multiplier1.5, risk_pct0.02): 输入标准OHLCV DataFrame返回带signal和position的新DataFrame signal: 1为持多-1为持空0为空仓 position: 目标持仓手数考虑ATR过滤后 df df.copy() # 计算双均线 df[ma_fast] df[close].rolling(fast).mean() df[ma_slow] df[close].rolling(slow).mean() # 计算ATR真实波幅的14日平均值 df[prev_close] df[close].shift(1) df[tr] np.maximum( df[high] - df[low], np.maximum( abs(df[high] - df[prev_close]), abs(df[low] - df[prev_close]) ) ) df[atr] df[tr].rolling(atr_period).mean() # 原始信号金叉做多死叉做空 df[raw_signal] 0 df.loc[df[ma_fast] df[ma_slow], raw_signal] 1 df.loc[df[ma_fast] df[ma_slow], raw_signal] -1 # ATR过滤当ATR低于历史均值的某个比例时视为震荡不开新仓 df[atr_ma] df[atr].rolling(atr_period * 3).mean() df[filter] df[atr] atr_multiplier * df[atr_ma] df[signal] df[raw_signal] * df[filter].astype(int) df[signal] df[signal].replace(0, np.nan) df[signal] df[signal].ffill().fillna(0) # 持仓延续直到反向信号 # 资金管理以当前ATR作为每手波动估算目标亏损不超过账户2% # 假设账户资金每变动1万元算一手波动这里仅演示原理 account_equity 1000000 # 初始资金100万 per_trade_loss_limit account_equity * risk_pct # 单笔最大亏损2万 # 每手波动价值 ATR * 合约乘数螺纹钢为10吨/手 contract_multiplier 10 position_size per_trade_loss_limit / (df[atr] * contract_multiplier) df[position] np.where(df[signal] ! 0, np.floor(position_size), 0) # 去除前期均线未形成时的NaN df.dropna(subset[ma_fast, ma_slow, atr], inplaceTrue) return df[[datetime, open, high, low, close, volume, signal, position, atr]] # 示例执行假设已经把parquet数据载入df result add_signal_and_position(loaded_df) print(result.tail())这段代码参数集中在fast、slow、atr_period、atr_multiplier和risk_pct。fast5, slow20是常见的短期趋势参数适合螺纹钢这种日线波动大的品种如果是豆粕这种相对温和的可以把slow拉到30。atr_multiplier1.5的意思是当前ATR低于其90日均值的1.5倍时过滤掉开仓信号。这个值设太高会错过行情设太低过滤不了震荡得靠回测去调。risk_pct0.02表示每笔交易最大亏损为账户净值2%这是期货交易里常见的风险控制标准你能守住这个底限至少不会爆仓。一个容易踩的细节我用ffill()把信号向前填充只要没有反向信号就维持原持仓。这模拟了实盘中不止盈只跟趋势的做法优点是不错过单边行情缺点是在回归行情中利润回吐大。你可以改成连亏N次就不交易之类的附加规则但别加太多期货策略参数越多过拟合风险越大。3.3 参数怎么设别迷信默认值很多人的策略脚本从头到尾用同一组参数跑所有品种——这等于让一个穿40码鞋的人去穿所有人的鞋。螺纹钢的日均波幅和豆粕完全不是一个量级ATR的值差了三倍你用到position_size上的结果天差地别。所以必须分品种做参数网格搜索。网格搜索的代码很简单就是把策略函数套在循环里用回测结果选参数。但注意两个陷阱一是参数步长不要太小否则会选到噪声二是别只看收益最高那组要看收益和回撤的比值。我一般用年化收益/最大回撤大于3作为候选门槛低于3的基本不考虑。4. 回测引擎自己写个逐K线循环比神棍回测靠谱一百倍4.1 核心循环别用向量化一步到位网上有些人的回测代码把信号算完然后用shift模拟买卖点向量化一条龙看起来又快又爽。但在期货里你的信号是基于收盘价算出来的实盘你只能在下一个K线开盘价成交。如果用收盘价撮合相当于你偷看了未来一根K线的收盘价回测收益虚高得离谱。这就是典型的未来函数。正确做法是逐根K线循环每一根K线只使用已经出现的信息信号产生后在下一根K线的开盘价成交。代码上并不复杂import pandas as pd def backtest(df, initial_capital1000000, fee_per_lot3, slip_points1, contract_multiplier10): 逐K线回测引擎 df: 包含 signal, position, open, high, low, close, atr 的DataFrame fee_per_lot: 每手手续费单边 slip_points: 滑点价格跳动点数 contract_multiplier: 合约乘数每点价值 df df.copy() df[equity] initial_capital df[cash] initial_capital df[position_value] 0.0 df[trade] 0 current_position 0 cash initial_capital entry_price 0.0 for i in range(1, len(df)): bar df.iloc[i] prev_bar df.iloc[i-1] # 如果上根K线产生信号且当前持仓与目标不同则在本根K线开盘价成交 target_position int(prev_bar[position]) if target_position ! current_position: # 以开盘价加上滑点作为成交价 if target_position current_position: # 开多或平空 fill_price bar[open] slip_points else: # 开空或平多 fill_price bar[open] - slip_points # 平掉旧仓位计算盈亏 if current_position ! 0: if current_position 0: pnl (fill_price - entry_price) * current_position * contract_multiplier else: pnl (entry_price - fill_price) * abs(current_position) * contract_multiplier cash pnl cash - fee_per_lot * abs(current_position) # 平仓手续费 # 开新仓 if target_position ! 0: margin_required abs(target_position) * fill_price * contract_multiplier * 0.1 cash - margin_required # 预留保证金 entry_price fill_price cash - fee_per_lot * abs(target_position) # 开仓手续费 current_position target_position df.at[df.index[i], trade] 1 # 更新持仓市值和账户净值 if current_position ! 0: if current_position 0: position_value (bar[close] - entry_price) * current_position * contract_multiplier else: position_value (entry_price - bar[close]) * abs(current_position) * contract_multiplier df.at[df.index[i], position_value] position_value df.at[df.index[i], cash] cash df.at[df.index[i], equity] cash position_value df.dropna(subset[open], inplaceTrue) return df这个循环每一行都有注释。注意几个参数fee_per_lot按每手固定费用算期货很多品种是按手数收费的比如螺纹钢开平仓各3元也有一部分是按成交额万分之几收你改成比例费率即可。slip_points设1意思是最坏情况下多花一个跳的价差成交。不要设0那是理想世界。margin_required那里我按10%保证金预设实际每个品种保证金比例不同但用作回测算个大概就行。4.2 手续费和滑点这两项不设回测就是自欺欺人有次我看到一个人回测螺纹钢五年年化收益率500%一问手续费设0、滑点设0。这种回测除了让他自己开心没有任何参考价值。期货是T0交易一天可能来回十几次手续费滑点累加起来极其恐怖。我把螺纹钢的典型成本写给你开仓手续费3元/手、平仓3元/手滑点一跳就是1个点位合约乘数是10即每跳10元。假设你一天交易10次光滑点就100元手续费60元一个月22个交易日成本就是3520元。如果账户本金只有10万这相当于每月3.5%的成本要从收益里先扣掉。所以回测代码里fee_per_lot和slip_points必须设成真实值。你可以稍微保守一点滑点设2跳手续费按交易所两倍算这样回测结果如果还能盈利实盘才有底气。4.3 绩效指标年化、夏普、最大回撤怎么算回测跑完光看净值曲线没用你得算几个标准指标年化收益率、夏普比率、最大回撤、胜率和盈亏比。特别是最大回撤它直接关系到你心理能不能扛住。计算公式不复杂def calculate_metrics(df, risk_free_rate0.02, trading_days_per_year244): 计算回测绩效指标 daily_returns df[equity].pct_change().dropna() total_return df[equity].iloc[-1] / df[equity].iloc[0] - 1 years len(df) / trading_days_per_year annual_return (1 total_return) ** (1 / years) - 1 if years 0 else 0 # 夏普比率 excess daily_returns - risk_free_rate / trading_days_per_year sharpe np.sqrt(trading_days_per_year) * excess.mean() / (excess.std() 1e-8) # 最大回撤 cumulative df[equity].cummax() drawdown (df[equity] - cumulative) / cumulative max_drawdown drawdown.min() # 胜率按每笔交易统计这里简化为日收益率为正的天数占比 win_rate (daily_returns 0).sum() / len(daily_returns) return { annual_return: annual_return, sharpe: sharpe, max_drawdown: max_drawdown, win_rate: win_rate, total_transactions: int(df[trade].sum()) } metrics calculate_metrics(backtest_result) print(metrics)这里trading_days_per_year对期货一般是244天扣除节假日。max_drawdown是负值比如-0.25表示最大回撤25%。夏普比率低于1的策略基本不值得做高于1.5就算优秀。但注意回测时间太短算出的夏普没有意义至少跑三年以上。另外win_rate只看日期胜率其实不够严谨更准确的是按每笔完整交易计算但作为快速评估够用了。5. 避坑指南期货量化的五个血泪坑5.1 未来函数回测曲线把自己骗嗨了现象策略在回测里年化翻倍实盘一个月亏掉20%。查代码才发现信号计算里用了close.shift(-1)也就是用未来K线的收盘价做计算。原因大多数新手写回测时没有区分信号产生时点和可成交时点。比如你在回测代码里写df[signal] df[ma_fast] df[ma_slow]这行代码是在整根K线结束后才计算的如果你在同一根K线上用收盘价买入实际成交价假设为收盘价那就偷看了未来。解决把信号计算统一放在K线结束时信号产生后只能在下一根K线的价格上成交。上面第4章的回测循环里我用prev_bar[signal]来判断当前K线的目标仓位就是强制隔离了一根K线的时间差。另一个常被忽略的是指标计算顺序比如你在计算均线时用了当前收盘价然后又用同一根K线的最高价去判断止损这也会形成未来函数。最严格的验证方法把回测结果里的每一笔交易打印出来人工检查K线图看是不是在信号K线的下一根才成交。5.2 换月跳空均线被拉出假金叉现象程序一直用的是RB0主力连续但每年4月、9月合约切换K线图上会出现巨大的跳空缺口。你的双均线原本是空头排列跳空后价格瞬间上移直接金叉于是系统开多结果一进场就遇到回归被套。原因主力连续合约把不同月份的合约拼接在一起但拼接时没有做复权价格差就是跳空。不处理的话任何基于价格均线的指标都会被污染。解决在前面数据层已经提过统一使用后复权数据。需要再次强调回测和实盘必须使用同一套复权规则否则实盘信号会完全错位。另外最好在代码里记录每次换月的日期换月前后一两个交易日暂停开新仓只允许平仓避免在跳空区域追单。5.3 滑点比想象中大十倍流动性判断错误现象回测滑点设1个跳实盘发现每次下单成交价都比预期差5到8个跳尤其到止损时一砸盘就瞬间滑出去10跳。原因不同品种的流动性天差地别。螺纹钢主力合约盘口挂单厚滑点小但比如晚盘某些冷门时间段盘口只有几手你一手20吨砸进去直接击穿买一卖一。另外止损单在暴跌行情中会因为跳空而滑得更远这是回测没体现出来的。解决做品种流动性评估。最简单的办法是看持仓量和成交量日均值低于某阈值的品种直接不要碰。滑点参数按品种分别设流动性差的品种设5跳以上流动性好的设2跳。如果做分钟线策略还要考虑你下单频率不能高于盘口恢复速度一般每5秒至少给市场留出喘息时间。5.4 保证金计算错误回测盈利实盘爆仓现象回测里账户净值一路向上实盘却接到追加保证金通知才发现持仓手数和账户资金严重不匹配。原因很多新手在计算仓位时直接用账户资金×仓位比例/合约价值得出手数但完全没考虑保证金比例和浮亏。期货杠杆是10倍左右你满仓操作价格反向波动10%就亏光本金。更隐蔽的是回测里的equity是浮盈浮亏都算进去的但如果浮亏过大会先触发强平根本等不到回测模型里的止损价。解决仓位上限必须控制在实际保证金的20%以内也就是杠杆使用率不超过2倍。上面策略代码里position_size的计算方式只是示例你需要改成真正的风险预算模型单笔止损金额账户权益×风险比例然后除以每手止损距离对应的合约价值。还有一点实盘账户初始资金和回测初始资金不一致时要按动态权益重新计算持仓手数不能简单用固定手数。5.5 程序跑起来内存暴涨数据帧死循环现象回测跑着跑着内存占用从1G涨到16G电脑风扇狂转最后进程被杀。原因常见的两个地方一是循环里不断用df.append()或者df df.append()每次追加都会复制整个DataFrame复杂度O(n²)。二是用了df.loc[i]在循环内部再修改另一个DataFrame的副本没有释放引用。我见过有人回测三年日线不到800行数据却因为用append写了10万行内存直接爆掉。解决循环里禁止append改用Python列表收集结果循环结束后一次性pd.DataFrame(list)。另外回测中产生的中间列如ma_fast、atr在每次迭代后及时del或直接覆盖。如果数据量实在大用NumPy数组存日线字段只把最终结果转成DataFrame。最好的办法是使用循环外推而非循环内改比如第4章的回测引擎里我把每次迭代的结果写入列表最后再构造DataFrame这样内存占用是稳定的。6. 把系统接到模拟盘验证从回测到实战的最后一公里回测再漂亮也只是在历史数据上做了一次事后诸葛的演练。真正要验证策略能不能生存必须接到模拟交易账户上跑至少两周。对接模拟盘的方式很多这里说一种比较省事的使用vn.py或者ctpbee对接CTP的模拟环境或者直接用第三方量化平台提供的模拟交易接口。模拟盘第一步是配置交易接口。以vn.py为例最小化的对接代码大致如下from vnpy.event import EventEngine from vnpy.trader.engine import MainEngine from vnpy.gateway.ctp import CtpGateway from vnpy.app.cta_strategy import CtaStrategyApp # 创建事件引擎和主引擎 event_engine EventEngine() main_engine MainEngine(event_engine) # 添加CTP接口和策略应用 main_engine.add_app(CtaStrategyApp) main_engine.add_gateway(CtpGateway) # 连接模拟账户把以下连接参数换成模拟环境的地址和账号 setting { 用户名: 你的模拟账号, 密码: 模拟密码, 经纪商代码: 9999, 交易服务器: tcp://simnow标准地址:端口, 行情服务器: tcp://simnow行情地址:端口, 产品名称: simnow_client_test, 授权编码: 0000000000000000, 产品信息: } main_engine.connect(setting, CTP)这一段代码在实际运行前需要你先到模拟柜台申请账号不同模拟环境的服务器地址和端口都不同不要照抄网上的过时信息。连接成功后还需要把策略逻辑从回测脚本改装成事件驱动的策略类因为回测是逐K线循环而实盘是逐笔推送成交。这一步是很多人从回测跨到实盘时最卡壳的地方——策略逻辑要拆分成on_bar和on_tick回调仓位要等成交回报确认后再更新。还有一块必不可少的是定时任务和日志。期货日盘从9点开始策略程序必须在收盘后自动停掉第二天开盘前自动重启。我会用APScheduler来实现早上8点30分启动程序加载数据并预热指标下午3点后停止交易生成当日盈亏报告。日志要记录每一次信号、下单、成交回报和异常情况否则出问题都不知道去哪查。我一般把日志写到本地文件同时按天分割保留最近30天即可。接收模拟盘验证时还涉及到一个容易忽视的细节网络断线。期货交易时段经常出现网关连接失效的情况程序如果直接崩溃等你发现时已经错过了一整段行情。所以我习惯在入口处加一个看门狗没收到行情超过5秒就重连重连失败就电话告警。这些都是实盘环境里才有的问题回测代码里根本碰不到。从回测走到模拟盘的感受确实不一样。我第一次把策略跑上模拟盘时本以为会像回测一样稳定盈利结果前三天下单就出现了两次滑点异常一次是因为代码里把价格取成了快照价而不是最新价另一次是因为计算仓位时忘了扣除保证金占用。从那以后我每次上线新策略都会强制走一遍完整的白名单流程先用历史数据把回测跑三遍再在模拟盘上连跑两周期间每天比对模拟盘的成交和回测计划是否一致不一致就直接停程序查代码绝不让它带病运行。这个过程很枯燥但确实是保命的规矩。希望这套资源里的数据和框架能帮你少走这些弯路。本文还有配套的精品资源点击获取