这篇文章记录了我开发一个黑色类期货交易策略的全过程,包括各种试错、推翻重来、半夜改bug的真实经历。如果你也在搞量化交易,希望这些弯路能帮你少走几步。
前段时间在看期货行情的时候,发现了一个挺有意思的事:铁矿石、焦煤、螺纹钢、热卷这四个品种,大部分时候都是一起涨一起跌的。想想也对,毕竟都是一条产业链上的:
铁矿石 + 焦煤 → 炼钢 → 螺纹钢/热卷
就像一家人,有福同享有难同当。
但是有时候会出现这种情况:比如铁矿石突然暴涨,而其他三个还在原地踏步。这时候我就在想,这种”不听话”的现象是不是意味着什么?
有一天看到铁矿石单独拉了个大阳线,其他三个品种还在磨蹭,我就琢磨:会不会其他三个也会跟着涨?
果然,第二天螺纹钢和热卷就开始补涨了。
这让我产生了一个想法:能不能写个程序,专门抓这种”带头大哥”出现的时候,然后跟着买其他几个品种?
刚开始我的想法很简单粗暴: 1. 看这四个品种的价格变化 2. 如果有一个突然涨得特别猛(比其他的明显多),那它就是”带头大哥” 3. 马上买其他三个品种,等着它们跟涨 4. 涨够了就卖掉
听起来很简单,但是真要写成代码,问题就来了。
这个问题困扰了我好几天。最开始想用简单的价格涨幅,比如某个品种今天涨了3%,其他的只涨1%,那就算突破。
但试了一下发现不行,因为: - 有时候市场整体都在涨,3%可能不算什么 - 有时候市场很平静,1%的差异可能就很大了
后来想到用相对偏离的概念。就是看某个品种相对于”大家的平均表现”偏离了多少。
这个更头疼。市场每天都有波动,不能说稍微有点不一样就算突破吧?
想来想去,决定用统计学的方法: 1. 计算过去一段时间(比如30天)每个品种的”正常偏离范围” 2. 如果某天的偏离超过了这个范围,比如超过了2个标准差,那就算突破 3. 这时候其他品种就跟着这个”带头大哥”的方向操作
因为要接期货交易,最后选择了发明者量化平台,主要是因为: - 支持期货CTP接口 - 有现成的框架,不用从头搞 - JavaScript写起来比较顺手
第一版代码逻辑很简单:
// 伪代码
if (某个品种偏离 > 阈值) {
买其他三个品种();
}
if (盈利 > 止盈点 || 亏损 > 止损点) {
全部平仓();
}
回测结果惨不忍睹,胜率不到30%。仔细分析后发现几个问题:
经过反思,我意识到应该更加尊重市场的节奏。于是设计了一个状态机:
这个想法看起来简单,但实现起来需要一个完整的状态机。让我用代码来说明这个过程:
// 状态变量定义
var hasFoundInitialBalance = false; // 是否找到初始平衡
var waitingForBalance = false; // 是否在等待平衡恢复
var isInPosition = false; // 是否有持仓
// 平衡检测函数
function detectLeaderAndFollowers(ironOreR, cokeR, rebarR, hotRollR) {
// 计算各品种的同步得分
var syncScores = calculateSyncScores(ironOreR, cokeR, rebarR, hotRollR);
// 找出最大偏离的品种
var maxAbsScore = 0;
var leader = null;
Object.keys(syncScores).forEach(function(commodity) {
var absScore = Math.abs(syncScores[commodity]);
if (absScore > maxAbsScore) {
maxAbsScore = absScore;
if (absScore > syncThreshold) { // 超过2σ算突破
leader = commodity;
}
}
});
return {
hasLeader: leader !== null,
leader: leader,
maxDeviation: maxAbsScore,
isBalanced: maxAbsScore < balanceThreshold, // 小于1σ算平衡
syncScores: syncScores
};
}
// 核心状态机逻辑
if (!isInPosition) {
if (!hasFoundInitialBalance) {
// 状态1: 寻找初始平衡
if (leaderResult.isBalanced) {
hasFoundInitialBalance = true;
Log("✅ 找到初始平衡,开始监控突破机会", "#00FF00");
} else {
Log(`🔍 寻找平衡中...最大偏离: ${leaderResult.maxDeviation.toFixed(2)}σ`, "#FFAA00");
}
} else if (!waitingForBalance) {
// 状态2: 等待突破
if (leaderResult.hasLeader) {
// 发现突破!开仓其他三个品种
Log(`💥 ${leaderResult.leaderName}突破!跟随开仓`, "#FF0000");
// 跟随开仓逻辑
followers.forEach(function(follower) {
if (direction === 'BUY') {
p.OpenLong(contractMap[follower], Amount);
} else {
p.OpenShort(contractMap[follower], Amount);
}
});
isInPosition = true;
} else {
Log(`⚖️ 等待突破...最大偏离: ${leaderResult.maxDeviation.toFixed(2)}σ`, "#00AAFF");
}
} else {
// 状态4: 等待平衡恢复
if (leaderResult.isBalanced) {
Log("✅ 平衡恢复!准备下次机会", "#00FF00");
waitingForBalance = false; // 回到状态2
} else {
Log(`⏳ 等待平衡恢复...偏离: ${leaderResult.maxDeviation.toFixed(2)}σ`, "#FFAA00");
}
}
} else {
// 状态3: 持仓中,监控止盈止损
if (totalProfit > profitTarget || totalProfit < -stopLoss) {
Log("止盈止损,平仓!", "#0000FF");
p.CoverAll();
isInPosition = false;
waitingForBalance = true; // 进入状态4
}
}
这个状态机的精妙之处在于:
强制等待平衡:程序启动后不会立即交易,而是先观察市场,等到四个品种都相对平静(最大偏离σ)才开始监控。
确认真突破:只有在平衡状态下的突破(>2σ)才认为是有效信号,避免在震荡行情中被假突破骗。
防止频繁交易:平仓后必须等平衡恢复才能开新仓,给市场足够的”冷静时间”。
状态可视化:每个状态都有明确的日志输出,方便调试和监控。
实际运行时,你会看到这样的日志:
🔍 寻找平衡中...最大偏离: 1.8σ
✅ 找到初始平衡,开始监控突破机会
⚖️ 等待突破...最大偏离: 0.5σ
💥 铁矿石突破!跟随开仓
📊 持仓中...当前盈亏: +1200
止盈止损,平仓!
⏳ 等待平衡恢复...偏离: 1.5σ
✅ 平衡恢复!准备下次机会
这样的设计大大提高了策略的稳定性和胜率。
不是随便偏离一点就开仓,而是: - 首先要确认市场处于平衡状态 - 然后等待明确的突破信号(超过2个标准差) - 最后才跟随开仓
改成纯粹的止盈止损,不再考虑”平衡恢复”平仓,因为发现这样经常过早离场。
平仓后必须等待市场重新平衡才能开下一次仓,避免来回打脸。
JavaScript的变量作用域真的是个坑,写着写着就报错:ReferenceError: xxx is not defined
。
最后发现是全局变量和局部变量搞混了,索性把所有状态变量都放到main函数里面定义,问题解决。
刚开始没考虑数据不足的情况,结果程序一启动就报错。后来加了各种边界条件判断:
if (minLength < syncWindow + 5) {
return {
hasLeader: false,
message: "数据不足,无法检测"
};
}
期货有主力合约切换的问题,之前的代码没处理,导致持仓莫名其妙地变了。后来专门写了个posTrans
函数处理合约转换。
function posTrans(p, mainList) {
var codeList = mainList.map(item => item.match(/[A-Za-z]+/g)[0]);
var prePos = exchange.GetPosition();
prePos.forEach(function(pos) {
var mainCode = pos.ContractType.match(/[A-Za-z]+/g)[0];
if (mainList.indexOf(pos.ContractType) === -1) {
var index = codeList.indexOf(mainCode);
var mainID = index !== -1 ? mainList[index] : null;
if (mainID) {
Log('旧合约', pos.ContractType, '需要被更换为', mainID);
p.Cover(pos.ContractType);
if (pos.Type === PD_LONG || pos.Type === PD_LONG_YD) {
p.OpenLong(mainID, pos.Amount);
} else {
p.OpenShort(mainID, pos.Amount);
}
}
}
});
var afterPos = exchange.GetPosition();
if (afterPos.every(pos => mainList.indexOf(pos.ContractType) !== -1)) {
Log("所有合约都是主力合约", "#00FF00");
}
}
最终的策略核心是这样的:
计算同步得分:
状态判断:
交易决策:
经过大量回测调优,最终确定的参数:
var syncWindow = 30; // 30天计算窗口
var syncThreshold = 2.0; // 2σ突破阈值
var balanceThreshold = 1.0; // 1σ平衡阈值
var profitTarget = 3000; // 止盈点
var stopLoss = 1500; // 止损点
使用近3年的数据进行回测,表现还是相当比较稳定的,大家可以参考一下。
这个策略从想法到实现,前前后后折腾了大概两个月。期间推翻重来了好几次,也踩了不少坑。最大的感受是:量化交易看起来高大上,其实就是把交易直觉用代码实现出来。关键是要保持耐心,不断测试和改进。另外,没有完美的策略。市场在变,策略也要跟着调整。重要的是建立一套完整的开发和测试流程,这样才能持续改进。
如果你也在搞量化交易,希望这个经历对你有帮助。有问题可以一起交流,毕竟一个人折腾太孤单了😄
写这篇文章主要是想和大家分享一下开发策略的真实过程,包括那些踩过的坑和走过的弯路。如果对你有帮助,或者你有不同的看法,欢迎在评论区多多交流!
如果大家对这个策略比较感兴趣,评论多的话,我可以考虑把完整的源码开源出来,包括: - 完整的策略代码 - 详细的参数调优过程 - 回测报告和风险分析 - 实盘运行的注意事项
量化交易这条路不好走,但一群人走总比一个人走要有趣得多。期待和大家的交流!
声明:本文仅为技术分享,不构成投资建议。期货交易有风险,入市需谨慎。