MT5连续期货合约拼接实战
用自定义指标合成平滑可分析的期货历史
为什么需要连续期货合约
在 MetaTrader 5 中,交易者不能自行创建新的图表品种,图表只能由经纪商(交易商)在服务器端生成。对于期货品种而言,每一个合约都有明确的到期日,到期后退市的合约历史虽然仍保留在客户端,但分散在不同品种符号下,例如 UX-9.13、UX-12.13、UX-3.14 等。交易者若想观察长期价格走势,需要在不同合约之间来回切换,极为不便。连续期货合约(Continuous Futures)便是将多个临近到期的期货品种的历史数据按一定规则衔接起来,形成一条看似不间断的价格曲线,便于趋势分析和指标叠加。
问题在于,只有交易商能够在服务器端决定是否将不同月份的期货连接到同一个品种名之下。幸运的是,已关闭期货的历史数据会一直存在于客户端本地,因此我们完全可以基于这些本地历史,在客户端通过自定义指标的方式自行拼接出连续合约图表,而不依赖交易商的支持。
期货合约拼接的核心难题
在拼接不同月份的期货时,交易者会面临两个典型问题。第一个是日期覆盖:临近期货的交易日期存在重叠。例如 UX-3.14 期货在 2013.09.16 就开始交易,但此时 UX-12.13 期货尚未到期,仍处在交易状态。这种重叠意味着在某个时间段内,市场上同时存在两个活跃的合约,拼接时必须决定以哪一个的价格为准。
第二个问题是拼接方式的选择。如果处理不当,拼接后的图表会出现价格跳空或不够平滑,影响技术指标的计算。通常有两种基本拼接思路:简单追加和偏移追加。理解这两种方式的差异,是构建连续合约指标的关键。
- 日期覆盖:新旧合约同时交易造成的区间重叠
- 拼接断裂:合约切换点出现价格跳空或不连续
- 历史加载:需确保各合约历史数据完整复制到本地
简单追加与偏移追加算法对比
简单追加(Simple Addition)是指当前品种循环日期过期后,且新品种已经开盘,才将新品种的历史柱线直接接在旧品种之后。这种方法实现简单,价格数值与对应品种完全吻合,不会扭曲历史真实价格。但缺点是在合约切换的特殊位置,由于两个合约的收盘价可能存在天然价差,图表上会一直存在价格间隙,不够平滑,可能导致均线类指标在此处产生毛刺。
偏移追加(Addition With Shift)则更为巧妙:在当前品种还有 5 到 10 日才关闭时,就提前用下个品种的柱线替代这最后的 5-10 根柱线,这段过渡周期能让图表衔接更为平滑。其代价是过渡周期显示的价格数值与当前品种实际不符(相当于引入了下一合约的价格),但对于注重形态连贯性的分析而言,平滑性往往比局部真实价格更重要。
客户端环境与指标文件组织
在动手编写指标前,需先规划好文件结构。MetaTrader 5 默认指标位于 客户端数据文件夹\MQL5\Indicators 下。为避免与系统标准库混淆,建议在 Indicators 内新建 MyIndicators\Synthetics 文件夹,最终路径为 客户端数据文件夹\MQL5\Indicators\MyIndicators\Synthetics。这样便于通过 MQL5 存储进行同步,也方便后续维护。
在市场报价窗口中,相关期货品种应降序排列(例如 UX-3.14 在 UX-12.13 之上),因为指标代码通过 SymbolName(n,true) 按市场报价中的顺序依次读取合约。若顺序错乱,拼接逻辑将取到错误的品种对。
SYNT 指标创建与参数设计
在 Synthetics 文件夹中新建自定义指标文件,命名为 SYNT。需设置两个关键输入参数:number_futures_gluing(接续期货数量,最小值 2,确定要连接的品种数量)和 gluing_type(胶合型,默认 simple_addition,可选 simple_addition 或 addition_with_shift)。指标应放置在分离窗口,使用 DRAW_COLOR_CANDLES 绘制彩色蜡烛,需要 4 个价格缓冲区和 1 个颜色索引缓冲区。
为了让 gluing_type 以下拉列表展示,必须在输入参数块前声明枚举类型 ENUM_GLUING_TYPE。编译后加载指标,即可在参数面板中看到拼接类型的下拉选择,而不是手动输入。
enum ENUM_GLUING_TYPE
{
simple_addition, // simple addition
addition_with_shift // addition with shift
};
input ENUM_GLUING_TYPE gluing_type=simple_addition;
指标缓冲区与初始化细节
SYNT 指标共使用 5 个缓冲区:OpenBuffer、HighBuffer、LowBuffer、CloseBuffer 用于 OHLC 价格,ColorCandlesColors 保存每根蜡烛的颜色索引,另有一个 LoadHistory 辅助数组。所有缓冲区通过 SetIndexBuffer 绑定,并调用 ArraySetAsSeries 设为时间序列(最新柱在索引 0)。
一个易错点是数组初始化位置。很多开发者习惯在 OnInit 中初始化缓冲区,但 OnInit 仅在指标首次加载或用户点击“刷新”时触发,而 OnCalculate 在任意情况下都会发生。因此,强制初始化必须在 OnCalculate 中、且当 prev_calculated==0 或历史变更时执行 ArrayInitialize 置零。若尝试用 EMPTY_VALUE 初始化,将导致拼接失败。
SetIndexBuffer(0,OpenBuffer,INDICATOR_DATA); SetIndexBuffer(1,HighBuffer,INDICATOR_DATA); SetIndexBuffer(2,LowBuffer,INDICATOR_DATA); SetIndexBuffer(3,CloseBuffer,INDICATOR_DATA); SetIndexBuffer(4,ColorCandlesColors,INDICATOR_COLOR_INDEX); ArraySetAsSeries(OpenBuffer,true); ArraySetAsSeries(HighBuffer,true);
简单追加函数实现原理
SimpleAddition 函数接收上边期货 simbUP、下边期货 simbDOWN、周期和颜色。它通过 SymbolInfoInteger 获取品种到期时间,若未到期则取本地时间作为截止。然后以 simbDOWN 的到期时间(或 simbUP 的开始时间)为起点,复制 simbUP 的 MqlRates 到缓冲区,并偏移 shift_array 写入。写完后 shift_array 累加,置 indicator_rendered 为 true 并重绘图表。
bool SimpleAddition(string simbUP,string simbDOWN,ENUM_TIMEFRAMES period,int Color)
{
datetime expiration_time_UP=int(SymbolInfoInteger(simbUP,SYMBOL_EXPIRATION_TIME));
if(expiration_time_UP>TimeLocal()) expiration_time_UP=TimeLocal();
expiration_time_DOWN=(simbDOWN!="")?int(SymbolInfoInteger(simbDOWN,SYMBOL_EXPIRATION_TIME)):int(SymbolInfoInteger(simbUP,SYMBOL_START_TIME));
MqlRates rates[]; ArraySetAsSeries(rates,true);
int copied=CopyRates(simbUP,period,expiration_time_DOWN,expiration_time_UP,rates);
if(copied>0)
{
for(int j=shift_array;j<shift_array+copied;j++)
{
OpenBuffer[j]=rates[j-shift_array].open; HighBuffer[j]=rates[j-shift_array].high;
LowBuffer[j]=rates[j-shift_array].low; CloseBuffer[j]=rates[j-shift_array].close;
ColorCandlesColors[j]=Color;
}
shift_array+=copied; indicator_rendered=true; ChartRedraw();
}
return(copied>0);
}
偏移追加与历史预加载
AdditionWithShift 与 SimpleAddition 逻辑几乎一致,唯一区别是从到期日减去 10 天(86400*10 秒),从而提前衔接。由于差异微小,原文建议保留两个独立函数而非合并,以便未来修改某一种算法或用于测试。CheckLoadHistory 函数则负责将所有品种历史复制到 tmp_rates 辅助数组,成功后将 good_history 标志置 true,OnTimer 才允许绘制。
expiration_time_UP=int(SymbolInfoInteger(simbUP,SYMBOL_EXPIRATION_TIME))-86400*10; expiration_time_DOWN=int(SymbolInfoInteger(simbDOWN,SYMBOL_EXPIRATION_TIME))-86400*10;
定时器主逻辑与指标加载
SYNT 指标核心绘制在 OnTimer 中,定时器间隔设为 3 秒。若 indicator_rendered 为 true 则直接返回;若 good_history 为真,按 gluing_type 分支循环调用 SimpleAddition 或 AdditionWithShift,颜色由 MathRand 随机取模生成;否则循环调用 CheckLoadHistory 预加载。指标最好加载到已关闭品种上并切到 H1 周期观察。
void OnTimer()
{
if(indicator_rendered==true) return;
if(good_history==true)
{
int t=0; int number;
switch(gluing_type)
{
case simple_addition:
for(int n=0;n<numder_futures_gluing;n++)
{ number=MathRand(); t=number%8; SimpleAddition(SymbolName(n,true),SymbolName(n+1,true),PERIOD_D1,t); }
break;
case addition_with_shift:
for(int n=0;n<numder_futures_gluing;n++)
{ number=MathRand(); t=number%8; AdditionWithShift(SymbolName(n,true),SymbolName(n+1,true),PERIOD_D1,t); }
break;
}
}
else { for(int n=0;n<numder_futures_gluing;n++) CheckLoadHistory(SymbolName(n,true),PERIOD_D1); }
}
拼接其他自定义指标的可能性
SYNT 本身作为自定义指标,可与其他指标通过“以前的指标数据”选项拼接。在导航器窗口的自定义指标中找到目标指标(如 AMA、BB、DEMA 等),拖到 SYNT 窗口,参数中选“以前的指标数据”即可。这样能在连续期货图上直接叠加均线类指标,但需注意并非所有指标都兼容,文章验证可用的包括 AMA、BB、自定义移动平均、DEMA、FrAMA、TEMA。
实操步骤与常见坑总结
综合来看,构建连续期货合约图表的步骤为:1)在市场报价按降序排列期货;2)建 Synthetics 文件夹与 SYNT 指标;3)声明枚举与输入输出参数;4)绑定缓冲区并在 OnCalculate 初始化;5)实现两种拼接函数与历史加载;6)OnTimer 中绘制并加载到图表。常见坑包括:市场报价顺序错误、在 OnInit 而非 OnCalculate 初始化数组、用 EMPTY_VALUE 初始化缓冲、忽略日期覆盖导致重叠期价格混乱、试图在策略测试器中使用该指标做连续合约回测。