☰
Python期货量化交易系统实战:从数据清洗到策略回测与模拟盘验证
2026/10/2 9:05:34 网站建设 项目流程

简介:Python期货量化交易系统是一套面向金融科技学习者与高校学生的完整项目资源,融合深度学习与人工智能算法实现期货行情分析与自动化交易决策,适合作为毕业设计或课程设计选题。压缩包共120个文件,主体为71个Python脚本,承担数据服务、策略回测与接口封装,另有C++头文件与动态库用于对接期货交易API,以及配置、说明和安全文档,包体仅6.89MB,结构紧凑便于本地部署。系统内置数据服务启停脚本与核心执行模块,封装交易策略、市场分析和订单执行逻辑,同时附带安装流程、安全策略和版本控制等说明,可支撑从环境配置到策略回测的完整实践链路。已有180人学习下载,对研究机器学习在量化交易中落地的开发者而言,这是一个可直接运行并二次扩展的高价值案例,尤其适合作为金融科技方向的实战练习。

1. Python期货量化交易系统:一套能让你跑通全流程的源码包

我在期货公司蹲过两年,见过太多手工交易员被来回打脸:日内盯盘盯出腱鞘炎,止损单没来得及敲进去,行情瞬间反向,账户直接趴窝。后来转战量化,最深的体会是——期货这玩意儿,T+0、多空双向、杠杆放大的特性,天生就适合用程序去捕捉规则,而不是靠人脑去追涨杀跌。这套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 # 拉取螺纹钢主力连续日线数据,adjust='qfq'是前复权,'hfq'是后复权 df = ak.futures_main_sina(symbol="RB0", start_date="20200101", end_date="20241201", adjust="hfq") # 统一列名,原始列名可能是中文,必须重命名 df.rename(columns={ "日期": "datetime", "开盘价": "open", "最高价": "high", "最低价": "low", "收盘价": "close", "成交量": "volume", "持仓量": "open_interest" }, inplace=True) # 去除停牌或空数据行 df.dropna(subset=["open", "close", "volume"], inplace=True) # 排序并重置索引,确保按时间升序 df.sort_values("datetime", inplace=True) df.reset_index(drop=True, inplace=True) # 只保留标准的OHLCV字段,日期转成datetime格式 df["datetime"] = pd.to_datetime(df["datetime"]) df = df[["datetime", "open", "high", "low", "close", "volume", "open_interest"]] print(df.head())

这段代码干了三件事:拉取数据、重命名、清洗。重点在adjust="hfq"这个参数,如果你写qfq,后续策略回测时历史价格会被换月后的新价格调整,相当于把未来信息带进了过去,回测结果虚高。symbol="RB0"是螺纹钢主力连续的代码,每个品种不一样,豆粕是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, compression="snappy") 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)

参数上,compression="snappy"是个平衡点:压缩率不是最高,但解压速度快,适合多次回测读取。文件路径建议按品种_周期的规则组织,比如RB0_daily.parquet、M0_15min.parquet,这样后续加品种不用重建目录结构。还需要说明的是,open_interest(持仓量)在期货里是有用的调仓信号,不要学股票量化把它扔了。

3. 策略层:双均线打底,加ATR和资金管理才敢实盘

3.1 为什么拿双均线开局

很多新手一上来就问"有没有深度学习模型直接预测涨跌",我给你的回答是:别闹。深度学习在期货上不是不能用,但那需要海量数据、特征工程和严谨的验证流程,不是你在Kaggle上跑个LSTM就能赚钱的。一套靠谱的期货策略,起步必须逻辑透明、参数可解释。双均线正好满足:快线上穿慢线开多,下穿开空,就这么简单。它解决的是趋势跟随问题,期货又是双向T+0,趋势策略天然有优势,所以它即便在2020年以后也依然有效——不是因为它能预测价格,而是因为它能保证你跟到大趋势。

但裸双均线有两个致命弱点:震荡行情里来回打脸,以及满仓进出导致回撤巨大。加上ATR过滤器和固定比例仓位管理,就能把这两个问题摁住。

3.2 完整策略代码:信号生成与仓位计算

核心逻辑分三步:计算均线和ATR,用ATR过滤掉窄幅震荡,然后再算目标仓位。代码里我直接把信号和仓位算在一起,方便你直接拿去回测:

import numpy as np import pandas as pd def add_signal_and_position(df, fast=5, slow=20, atr_period=14, atr_multiplier=1.5, risk_pct=0.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"], inplace=True) 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。fast=5, slow=20是常见的短期趋势参数,适合螺纹钢这种日线波动大的品种;如果是豆粕这种相对温和的,可以把slow拉到30。atr_multiplier=1.5的意思是:当前ATR低于其90日均值的1.5倍时,过滤掉开仓信号。这个值设太高会错过行情,设太低过滤不了震荡,得靠回测去调。risk_pct=0.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_capital=1000000, fee_per_lot=3, slip_points=1, contract_multiplier=10): """ 逐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"], inplace=True) return df

这个循环每一行都有注释。注意几个参数:fee_per_lot按每手固定费用算,期货很多品种是按手数收费的,比如螺纹钢开平仓各3元;也有一部分是按成交额万分之几收,你改成比例费率即可。slip_points设1,意思是最坏情况下多花一个跳的价差成交。不要设0,那是理想世界。margin_required那里我按10%保证金预设,实际每个品种保证金比例不同,但用作回测算个大概就行。

4.2 手续费和滑点:这两项不设,回测就是自欺欺人

有次我看到一个人回测螺纹钢五年,年化收益率500%,一问,手续费设0、滑点设0。这种回测除了让他自己开心,没有任何参考价值。期货是T+0交易,一天可能来回十几次,手续费滑点累加起来极其恐怖。我把螺纹钢的典型成本写给你:开仓手续费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_rate=0.02, trading_days_per_year=244): """计算回测绩效指标""" 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秒就重连,重连失败就电话告警。这些都是实盘环境里才有的问题,回测代码里根本碰不到。

从回测走到模拟盘的感受确实不一样。我第一次把策略跑上模拟盘时,本以为会像回测一样稳定盈利,结果前三天下单就出现了两次滑点异常,一次是因为代码里把价格取成了快照价而不是最新价,另一次是因为计算仓位时忘了扣除保证金占用。从那以后,我每次上线新策略都会强制走一遍完整的白名单流程:先用历史数据把回测跑三遍,再在模拟盘上连跑两周,期间每天比对模拟盘的成交和回测计划是否一致,不一致就直接停程序查代码,绝不让它带病运行。这个过程很枯燥,但确实是保命的规矩。希望这套资源里的数据和框架能帮你少走这些弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询