上一集你写了第一个突破策略。程序能看行情、能判断条件、能下单、能止盈止损。
但有一个问题你没回答:这个策略到底能不能赚钱?
你不知道。
也许能。也许不能。也许在螺纹钢上行,但在豆粕上亏。也许在2024年赚了30%,但在2025年亏了20%。
不知道。全凭感觉。
这就是大部分手动交易者的状态。他们的策略是"我觉得行",不是"数据告诉我行"。
这一集,我要教你怎么把"我觉得行"变成"数据告诉我行"。
这个东西叫回测(Backtesting)。它是程序化交易最核心的能力——没有之一。
想象你发明了一种扑克打法。你觉得这种打法很好,能在赌桌上赢钱。
你怎么验证?
方法一:上去直接打,用真钱。赢了当然好,输了——你的钱就没了。
方法二:找过去1000场牌局的记录,假装你当时在场,用你的打法"虚拟参与"每一局。看这1000局下来,你是赚了还是亏了。
回测就是方法二。
你不需要真金白银去试。你只需要拿过去的历史行情数据,让你的策略"虚拟"跑一遍,看结果。
如果过去三年你的策略能稳定赚钱,那它大概率在未来也能赚钱(不是100%保证,但比瞎猜强太多了)。
如果过去三年你的策略一直在亏——那你千万别拿真钱去试,先改策略。
这就是回测的价值:用低成本(零风险)验证你的策略。
做一次回测,你需要准备三个东西:
| 需要什么 | 说明 | 哪里弄 |
| 历史行情数据 | 过去的K线数据(开盘、最高、最低、收盘、成交量) | 交易所官网、期货数据网站、CTP自带的历史数据接口 |
| 策略代码 | 上一集写的突破策略 | 你已经有了 |
| 回测引擎 | 一个能"模拟"K线逐根推进的程序 | 我们自己写一个 |
下面,一个一个来。
你的策略做的是螺纹钢,所以你需要螺纹钢的历史K线数据。
数据长这样(CSV格式):
date,open,high,low,close,volume
2024-01-02,3850,3878,3842,3865,152340
2024-01-03,3866,3892,3858,3885,167890
2024-01-04,3887,3910,3870,3902,145230
...
每一行是一根日线K线:日期、开盘价、最高价、最低价、收盘价、成交量。
方法一:CTP接口拿(你已经有CTP了)
CTP行情服务器除了实时推送行情,还可以查询历史数据。但SimNow的历史数据有限,通常只保留最近几个月。
方法二:免费数据网站(推荐新手)
https://www.shinnytech.com/tqsdk/ — 免费注册,提供大量期货历史数据,Python直接调用pip install akshare — 开源财经数据接口,Python一行代码获取用AKShare下载螺纹钢历史数据:
import akshare as ak
下载螺纹钢主力合约的日线数据
df = ak.futures_zh_daily_sina(symbol="RB0")
RB0 = 螺纹钢主力合约(自动跟踪)
保存为CSV
df.to_csv("rb_daily.csv", index=False)
print(f"下载完成:{len(df)} 条数据")
三行代码,几秒钟就下载好了。通常是过去几年的日线数据,几千条,完全够用。
方法三:手动下载CSV
有些网站提供现成的CSV文件下载。搜索"螺纹钢历史日线数据 CSV"就能找到。
不管你用什么方法,最终目标都是拿到一个CSV文件,里面是日期、开高低收、成交量。
"回测引擎"听起来很高大上,其实就是一个循环:
for 每一根K线:
把这根K线喂给策略
策略判断:买?卖?不动?
记录结果
最后算总账
就这么简单。不需要什么框架,不需要什么黑科技。你自己写一个,50行代码搞定。
创建文件 backtest.py:
import pandas as pd
import matplotlib.pyplot as plt
====================================
读取历史数据
====================================
df = pd.read_csv("rb_daily.csv")
确保列名正确(不同数据源的列名可能不一样)
df.columns = ['date', 'open', 'high', 'low', 'close', 'volume', 'hold']
按日期排序
df = df.sort_values('date').reset_index(drop=True)
print(f"数据加载完成:{len(df)} 根K线")
print(f"日期范围:{df['date'].iloc[0]} 到 {df['date'].iloc[-1]}")
print("-" * 50)
====================================
策略参数
====================================
PROFIT_TARGET = 50 # 止盈50点
STOP_LOSS = 30 # 止损30点
INITIAL_CAPITAL = 100000 # 初始资金10万
====================================
回测核心循环
====================================
capital = INITIAL_CAPITAL
position = 0 # 0=空仓,1=持多
entry_price = 0 # 入场价
trades = [] # 记录每笔交易
equity_curve = [] # 资金曲线(每天的总资产)
for i in range(len(df)):
row = df.iloc[i]
# ---- 当天收盘时的总资产 ----
# 空仓:总资产 = 现金
# 持仓:总资产 = 现金 + 持仓市值
if position > 0:
# 螺纹钢1手=10吨,每跳1点=10元
unrealized_pnl = (row['close'] - entry_price) * 10 * position
total_equity = capital + unrealized_pnl
else:
total_equity = capital
equity_curve.append({
'date': row['date'],
'equity': total_equity,
'position': position
})
# ---- 空仓时:检查买入条件 ----
if position == 0 and i > 0:
prev = df.iloc[i - 1] # 上一根K线("昨天")
# 条件1:昨天是阳线(收盘 > 开盘)
is_yang = prev['close'] > prev['open']
# 条件2:今天最高价突破昨天高点
if is_yang and row['high'] > prev['high']:
# 用昨天高点+1点作为买入价(保守一点)
buy_price = prev['high'] + 1
margin = buy_price * 10 * 0.10 # 螺纹钢保证金约10%
if capital >= margin:
position = 1
entry_price = buy_price
capital -= margin # 扣除保证金
trades.append({
'date': row['date'],
'action': 'BUY',
'price': buy_price,
'reason': f'突破昨日高点 {prev["high"]}'
})
# ---- 持仓时:检查止盈止损 ----
if position > 0:
profit = row['high'] - entry_price # 用最高价判断(最乐观)
# 先检查止盈(价格先到了哪个就执行哪个)
if profit >= PROFIT_TARGET:
sell_price = entry_price + PROFIT_TARGET
pnl = (sell_price - entry_price) * 10 * position
capital += margin + pnl
trades.append({
'date': row['date'],
'action': 'SELL',
'price': sell_price,
'pnl': pnl,
'reason': f'止盈 +{PROFIT_TARGET}点'
})
position = 0
entry_price = 0
elif row['low'] - entry_price <= -STOP_LOSS:
sell_price = entry_price - STOP_LOSS
pnl = (sell_price - entry_price) * 10 * position
capital += margin + pnl
trades.append({
'date': row['date'],
'action': 'SELL',
'price': sell_price,
'pnl': pnl,
'reason': f'止损 -{STOP_LOSS}点'
})
position = 0
entry_price = 0
# ---- 日线结束时如果还持仓,按收盘价计算盈亏 ----
# (这里简化处理:不隔夜持仓,收盘强制平仓)
if position > 0:
sell_price = row['close']
pnl = (sell_price - entry_price) * 10 * position
capital += margin + pnl
trades.append({
'date': row['date'],
'action': 'SELL',
'price': sell_price,
'pnl': pnl,
'reason': '收盘平仓'
})
position = 0
entry_price = 0
====================================
输出结果
====================================
final_equity = equity_curve[-1]['equity']
total_return = (final_equity - INITIAL_CAPITAL) / INITIAL_CAPITAL * 100
print("=" * 50)
print("📊 回测结果")
print("=" * 50)
print(f"初始资金:{INITIAL_CAPITAL:,.0f} 元")
print(f"最终资金:{final_equity:,.0f} 元")
print(f"总收益率:{total_return:+.2f}%")
print(f"交易次数:{len(trades) // 2} 次({len(trades)} 笔)")
计算胜率
wins = sum(1 for t in trades if t.get('pnl', 0) > 0)
losses = sum(1 for t in trades if t.get('pnl', 0) < 0)
total_closed = wins + losses
if total_closed > 0:
print(f"盈利次数:{wins}")
print(f"亏损次数:{losses}")
print(f"胜率:{wins / total_closed * 100:.1f}%")
计算最大回撤
equity_series = [e['equity'] for e in equity_curve]
peak = equity_series[0]
max_drawdown = 0
for eq in equity_series:
if eq > peak:
peak = eq
dd = (peak - eq) / peak * 100
if dd > max_drawdown:
max_drawdown = dd
print(f"最大回撤:{max_drawdown:.2f}%")
打印所有交易记录
print("\n📋 交易记录:")
print("-" * 70)
print(f"{'日期':<12} {'操作':<6} {'价格':<10} {'盈亏':>10} {'原因'}")
print("-" * 70)
for t in trades:
pnl_str = f"{t.get('pnl', 0):+.0f}元" if 'pnl' in t else ""
print(f"{t['date']:<12} {t['action']:<6} {t['price']:<10.0f} {pnl_str:>10} {t['reason']}")
====================================
画资金曲线图
====================================
dates = [e['date'] for e in equity_curve]
equities = [e['equity'] for e in equity_curve]
plt.figure(figsize=(12, 6))
plt.plot(dates, equities, color='#FF6B35', linewidth=1.5)
plt.axhline(y=INITIAL_CAPITAL, color='#58A6FF', linestyle='--', alpha=0.5, label='初始资金')
plt.title('突破策略回测 - 资金曲线', fontsize=16)
plt.xlabel('日期', fontsize=12)
plt.ylabel('资金 (元)', fontsize=12)
plt.legend()
plt.grid(True, alpha=0.3)
旋转x轴标签(日期太多的话)
plt.xticks(rotation=45)
plt.tight_layout()
plt.savefig('backtest_result.png', dpi=150)
print(f"\n📈 资金曲线已保存:backtest_result.png")
df = pd.read_csv("rb_daily.csv")
df = df.sort_values('date')
把CSV读进来,按日期排序。确保K线是从早到晚排列的——回测必须从第一根开始,一根一根往后推。
for i in range(len(df)):
row = df.iloc[i]
# 判断买入/卖出...
这就像"时间机器"——你站在第一根K线开始,一天一天往后走。每一"天",你的策略看当天的数据,决定买不买、卖不卖。
if is_yang and row['high'] > prev['high']:
buy_price = prev['high'] + 1
跟上一集的策略一模一样:昨天阳线+今天突破昨天高点。回测里唯一不同的是——用 prev['high'] + 1 作为买入价,因为回测中我们假设"突破后以高于突破点1个点的价格成交"。
这叫滑点(Slippage)——真实交易中,你看到突破价的时候,那个价格已经过去了,你实际买到的价格通常比突破价高一点。回测中考虑滑点,结果更接近实盘。
if profit >= PROFIT_TARGET:
sell_price = entry_price + PROFIT_TARGET
elif row['low'] - entry_price <= -STOP_LOSS:
sell_price = entry_price - STOP_LOSS
用当天最高价判断止盈,最低价判断止损。这是一种简化——实际上止盈止损可能在盘中任意时刻触发,但日线回测我们只能用高低价来近似。
日线回测 vs 分钟线回测:
新手先用日线回测,验证大方向。后面有经验了再用分钟线回测做精细验证。
equity_curve.append({'date': row['date'], 'equity': total_equity})
每天记录一次你的"总资产"。把这些点连起来画成图,就是你的资金曲线——从第一天的10万到最后一天的总资产,中间所有起伏一目了然。
跑完回测,你会看到一堆数字。哪些才是重点?
最终资金 ÷ 初始资金 - 1 = 总收益率
比如10万变成12万,总收益率就是20%。
但光看收益率不够。你需要知道——这个收益率是在多长时间内取得的?如果是三年20%,年化才6%,不如存银行。如果是一年20%,那就不错了。
盈利次数 ÷ 总交易次数 = 胜率
很多新手迷信高胜率,觉得"80%的交易都赚钱"就是好策略。错。
一个胜率40%的策略,如果每次赚的钱是每次亏的钱的2倍(盈亏比2:1),长期也是赚钱的:
胜率 × 盈亏比才是关键,不是胜率本身。
资金曲线从最高点到最低点的最大跌幅。
比如你的资金从10万涨到了13万,然后跌到了10.5万,最大回撤就是 (13-10.5)/13 ≈ 19.2%。
最大回撤告诉你:最坏的情况下,你会亏多少?
20%回撤意味着你在某个阶段亏掉了20%的利润(或本金)。你能扛得住吗?如果你每次看到账户亏20%就受不了、开始怀疑策略、频繁改参数——那你需要降低仓位,或者选一个回撤更小的策略。
平均每笔盈利 ÷ 平均每笔亏损 = 盈亏比
我们设的止盈50点、止损30点,理论盈亏比 = 50÷30 ≈ 1.67。
但实际盈亏比通常比理论值低(因为滑点、手续费)。回测跑出来的才是真实的盈亏比。
总交易了多少次。
太少(比如一年只有3次)——结果可能有偶然性,说明策略太保守,机会太少。
太多(比如一年500次)——可能过度交易,大部分是小噪音,而且手续费会吃掉利润。
一般一年20-60次比较合理,具体看你的策略周期。
你用了"未来"的数据来判断"过去"的操作。
举个例子:你在回测代码里写了 if row['close'] > row['open'](用收盘价判断)。但现实中,收盘价要到收盘才知道。你不能在盘中用收盘价做判断——那叫"知道了答案再做题"。
正确做法:只用你能实时获取的数据(当前价、最新K线)。未来函数会让回测结果虚高,实际交易会让你大跌眼镜。
你调参数调到完美匹配了历史数据,但未来不适用。
比如你试了20组止盈止损参数,选了收益率最高的一组(止盈47点、止损23点)。这组参数可能恰好在这个时间段表现最好,但换个时间段就崩了。
防范方法:
回测里每一笔交易都有成本:
如果不扣这些成本,回测结果会比实盘好很多。新手最容易犯这个错误。
建议:回测里每笔交易扣2-3个点的滑点+实际手续费。
你用3个月的数据做回测,结果"年化收益率80%"——别信。3个月的数据太少,偶然性太大,什么结论都不可靠。
至少用一年的数据。两年更好。
跑完回测后,按这个框架判断策略是否值得上实盘:
| 条件 | 标准 |
| 总收益为正 | ✅ 必须满足 |
| 最大回撤 < 20% | ✅ 新手建议 |
| 胜率 > 35% | ✅ 配合盈亏比看 |
| 盈亏比 > 1.5 | ✅ 配合胜率看 |
| 交易次数 20-60次/年 | ✅ 太多太少都不好 |
| 最近一年仍在盈利 | ✅ 不能只有早年好 |
如果以上全部满足 → 可以小仓位实盘试跑。
如果有一项不满足 → 回去调策略或参数,再回测一遍。
如果多项不满足 → 换一个策略思路。
回测通过之后,不要直接大仓位实盘。按这个顺序来:
第一步:模拟盘验证(1-2周)
用SimNow模拟账户跑你的策略,跟实盘完全一样的环境,但用的是假钱。
看看:
第二步:小仓位实盘(1手,1-2个月)
模拟盘没问题了,切到实盘。但只做1手——就算全亏了也亏不了多少。
第三步:正常仓位
小仓位跑了1-2个月,跟回测结果差距不大(收益率在预期范围内),就可以用正常仓位了。
1. 回测就是用历史数据"虚拟"跑你的策略——零成本、零风险验证
2. 三个准备:历史数据 + 策略代码 + 回测引擎(50行代码)
3. 回测引擎的本质是一个循环:逐根K线推进,每根判断买卖
4. 五个核心指标:总收益率、胜率、最大回撤、盈亏比、交易次数
5. 四个陷阱:未来函数、过度拟合、忽略成本、样本太少
6. 实盘三步走:模拟盘 → 小仓位 → 正常仓位
五集走完了。你从一个"程序化交易是什么"的新手,到现在:
这五集给了你一个完整的入门路径——从概念到实操,从环境搭建到策略开发,从实盘下单到回测验证。
但程序化交易的路才刚开始。
后面你可以探索的方向:
这些都是进阶内容。如果你有兴趣继续深入,欢迎关注果子量化(gocxh.com),我们会持续分享。
记住一句话:程序化交易不是让你变富的魔法,而是让你不再凭感觉交易的工具。
工具有了,接下来看你怎么用。
感谢你读完了这个系列。如果你觉得有用,分享给身边做期货的朋友——也许你帮他省了两年弯路。
*本系列面向零基础读者。有疑问加微信 futures886 或在评论区留言。*
*果子量化(gocxh.com)— AI驱动的期货程序化交易平台*
果子量化 - AI驱动的期货程序化交易平台
官网:gocxh.com | 微信:futures886