avatar of ianzeng123 ianzeng123
关注 私信
1
关注
136
关注者

折腾一个黑色类期货策略:从想法到代码的真实记录

创建于: 2025-09-05 09:43:48, 更新于: 2025-09-05 23:13:21
comments   0
hits   5

折腾一个黑色类期货策略:从想法到代码的真实记录

写在前面

这篇文章记录了我开发一个黑色类期货交易策略的全过程,包括各种试错、推翻重来、半夜改bug的真实经历。如果你也在搞量化交易,希望这些弯路能帮你少走几步。

第一步:发现有趣的现象

最初的观察

前段时间在看期货行情的时候,发现了一个挺有意思的事:铁矿石、焦煤、螺纹钢、热卷这四个品种,大部分时候都是一起涨一起跌的。想想也对,毕竟都是一条产业链上的:

铁矿石 + 焦煤 → 炼钢 → 螺纹钢/热卷

就像一家人,有福同享有难同当。

突然的想法

但是有时候会出现这种情况:比如铁矿石突然暴涨,而其他三个还在原地踏步。这时候我就在想,这种”不听话”的现象是不是意味着什么?

有一天看到铁矿石单独拉了个大阳线,其他三个品种还在磨蹭,我就琢磨:会不会其他三个也会跟着涨?

果然,第二天螺纹钢和热卷就开始补涨了。

这让我产生了一个想法:能不能写个程序,专门抓这种”带头大哥”出现的时候,然后跟着买其他几个品种?

第二步:把想法变成逻辑

最初的思路

刚开始我的想法很简单粗暴: 1. 看这四个品种的价格变化 2. 如果有一个突然涨得特别猛(比其他的明显多),那它就是”带头大哥” 3. 马上买其他三个品种,等着它们跟涨 4. 涨够了就卖掉

听起来很简单,但是真要写成代码,问题就来了。

第一个难题:怎么定义”突然涨得特别猛”?

这个问题困扰了我好几天。最开始想用简单的价格涨幅,比如某个品种今天涨了3%,其他的只涨1%,那就算突破。

但试了一下发现不行,因为: - 有时候市场整体都在涨,3%可能不算什么 - 有时候市场很平静,1%的差异可能就很大了

后来想到用相对偏离的概念。就是看某个品种相对于”大家的平均表现”偏离了多少。

第二个难题:怎么判断是”正常波动”还是”真突破”?

这个更头疼。市场每天都有波动,不能说稍微有点不一样就算突破吧?

想来想去,决定用统计学的方法: 1. 计算过去一段时间(比如30天)每个品种的”正常偏离范围” 2. 如果某天的偏离超过了这个范围,比如超过了2个标准差,那就算突破 3. 这时候其他品种就跟着这个”带头大哥”的方向操作

第三步:写代码实现

技术方案选择

因为要接期货交易,最后选择了发明者量化平台,主要是因为: - 支持期货CTP接口 - 有现成的框架,不用从头搞 - JavaScript写起来比较顺手

第一版代码:简单粗暴

第一版代码逻辑很简单:

// 伪代码
if (某个品种偏离 > 阈值) {
    买其他三个品种();
}
if (盈利 > 止盈点 || 亏损 > 止损点) {
    全部平仓();
}

第一次测试:惨败

回测结果惨不忍睹,胜率不到30%。仔细分析后发现几个问题:

  1. 假突破太多:很多时候看起来突破了,结果第二天就回去了
  2. 追高杀跌:经常在高点买入,在低点止损
  3. 没有耐心:突破后立即开仓,没有确认

第四步:反思和改进

引入”平衡-突破-跟随”循环

经过反思,我意识到应该更加尊重市场的节奏。于是设计了一个状态机:

  1. 寻找平衡:先等市场进入相对平衡的状态
  2. 等待突破:平衡状态下,等待某个品种突破
  3. 跟随开仓:确认突破后,其他品种跟随开仓
  4. 止盈止损:达到条件后平仓
  5. 等待恢复平衡:平仓后等待市场重新平衡
  6. 重复循环:回到第2步

这个想法看起来简单,但实现起来需要一个完整的状态机。让我用代码来说明这个过程:

// 状态变量定义
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σ
✅ 平衡恢复!准备下次机会

这样的设计大大提高了策略的稳定性和胜率。

关键改进点

1. 严格的开仓条件

不是随便偏离一点就开仓,而是: - 首先要确认市场处于平衡状态 - 然后等待明确的突破信号(超过2个标准差) - 最后才跟随开仓

2. 更合理的平仓机制

改成纯粹的止盈止损,不再考虑”平衡恢复”平仓,因为发现这样经常过早离场。

3. 防止频繁交易

平仓后必须等待市场重新平衡才能开下一次仓,避免来回打脸。

第五步:代码实现的坑

坑1:变量作用域问题

JavaScript的变量作用域真的是个坑,写着写着就报错:ReferenceError: xxx is not defined

最后发现是全局变量和局部变量搞混了,索性把所有状态变量都放到main函数里面定义,问题解决。

坑2:数据不足的处理

刚开始没考虑数据不足的情况,结果程序一启动就报错。后来加了各种边界条件判断:

if (minLength < syncWindow + 5) {
    return { 
        hasLeader: false, 
        message: "数据不足,无法检测" 
    };
}

坑3:合约切换问题

期货有主力合约切换的问题,之前的代码没处理,导致持仓莫名其妙地变了。后来专门写了个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");
    }
}

第六步:最终策略逻辑

核心算法

最终的策略核心是这样的:

  1. 计算同步得分

    • 计算四个品种30天的收益率
    • 计算每个品种相对于组合平均的偏离
    • 用Z-Score标准化,得到同步得分
  2. 状态判断

    • 平衡状态:所有品种偏离 < 1σ
    • 突破状态:某个品种偏离 > 2σ
  3. 交易决策

    • 发现突破 → 其他三个品种跟随开仓
    • 止盈止损 → 全部平仓
    • 等待平衡恢复 → 准备下一轮

关键参数

经过大量回测调优,最终确定的参数:

var syncWindow = 30;        // 30天计算窗口
var syncThreshold = 2.0;    // 2σ突破阈值
var balanceThreshold = 1.0; // 1σ平衡阈值
var profitTarget = 3000;    // 止盈点
var stopLoss = 1500;        // 止损点

第七步:回测结果和思考

回测表现

使用近3年的数据进行回测,表现还是相当比较稳定的,大家可以参考一下。

折腾一个黑色类期货策略:从想法到代码的真实记录

策略优势

  1. 逻辑清晰:基于产业链内在联系,有经济学基础
  2. 风险可控:有明确的止损机制
  3. 适应性强:不依赖特定市场环境
  4. 交易频率适中:不会过度交易

需要注意的问题

  1. 数据质量:需要稳定的实时数据源
  2. 滑点成本:实盘交易要考虑滑点和手续费
  3. 极端行情:黑天鹅事件可能导致策略失效
  4. 资金管理:单次仓位不宜过大

写在最后

这个策略从想法到实现,前前后后折腾了大概两个月。期间推翻重来了好几次,也踩了不少坑。最大的感受是:量化交易看起来高大上,其实就是把交易直觉用代码实现出来。关键是要保持耐心,不断测试和改进。另外,没有完美的策略。市场在变,策略也要跟着调整。重要的是建立一套完整的开发和测试流程,这样才能持续改进。

如果你也在搞量化交易,希望这个经历对你有帮助。有问题可以一起交流,毕竟一个人折腾太孤单了😄

交流与分享

写这篇文章主要是想和大家分享一下开发策略的真实过程,包括那些踩过的坑和走过的弯路。如果对你有帮助,或者你有不同的看法,欢迎在评论区多多交流

如果大家对这个策略比较感兴趣,评论多的话,我可以考虑把完整的源码开源出来,包括: - 完整的策略代码 - 详细的参数调优过程 - 回测报告和风险分析 - 实盘运行的注意事项

量化交易这条路不好走,但一群人走总比一个人走要有趣得多。期待和大家的交流!


声明:本文仅为技术分享,不构成投资建议。期货交易有风险,入市需谨慎。

相关推荐