之前试水用豆包写过一段合并K线的代码,但是因为豆包搞反了MT5原始数组的数据下标方向,导致最终的合并是从后往前合并的。后来我干脆用工程方法重新来过,把代码拆成全局声明、合并函数、缓存函数、绘图函数五个模块,算上主程序,共五个模块,重新写了一遍。现在把它们贴出来请大家斧正。因为也是现学现卖,就免费给大家研究。
一、下面是mq5主入口程序代码:
- //+------------------------------------------------------------------+
- //| 论指标主入口 chanlun.mq5 |
- //| Copyright 2026,ZHAO. |
- //| qq317008881 |
- //+------------------------------------------------------------------+
- #property copyright "Copyright 2026,ZHAO."
- #property link "qq317008881"
- #property version "1.00"
- #property strict //严格语法声明
- #property indicator_chart_window //指标显示在当前窗口声明
- #property indicator_buffers 0
-
- // 引入模块
- #include "chanlun_defines.mqh"
- #include "fun_chanlun_merge.mqh"
- #include "fun_chanlun_cache.mqh"
- #include "draw_chanlun_merge.mqh"
-
- //+------------------------------------------------------------------+
- //| Custom indicator initialization function 加载初始化函数 |
- //+------------------------------------------------------------------+
- int OnInit ()
- {
- Print("chan指标开始!尝试从硬盘加载历史合并缓存。");
-
- // 尝试从硬盘加载历史合并缓存
- if(LoadMergeCache() == false)
- {
- Print("无有效缓存,首次计算将执行全量合并");
- }
- // 加载成功的话,g_mergeArray 里就已经有完整的历史合并结果了,后续只算增量
- // 加载失败也没关系,第一次 OnCalculate 会自动跑全量合并
-
- Print("chan指标初始化成功!");
-
- return INIT_SUCCEEDED;
- }
-
- //+------------------------------------------------------------------+
- //| Custom indicator iteration function “自定义指标”迭代函数 |
- //+------------------------------------------------------------------+
- int OnCalculate(const int32_t rates_total, // 1. 当前图表总共有多少根K线
- const int32_t prev_calculated, // 2. 上一次已经计算完成的K线数量(增量核心),含上次结束时未收盘的那根
- const datetime &time[], // 3. 每根K线的开盘时间数组
- const double &open[], // 4. 开盘价数组
- const double &high[], // 5. 最高价数组
- const double &low[], // 6. 最低价数组
- const double &close[], // 7. 收盘价数组
- const long &tick_volume[], // 8. tick成交量数组
- const long &volume[], // 9. 标准成交量数组
- const int32_t &spread[]) // 10. 点差数组
- {
- Print("当前K线图表加载成功,共加载K线=", ArraySize(time),"个。");
-
- // ==================================================
- // 分支1:首次加载 / 数据重置(prev_calculated == 0)
- // 触发场景:指标刚加载、切换品种周期、历史数据修正、拖拽加载更早K线
- // ==================================================
- if(prev_calculated == 0)
- {
- // 调用统一缓存校验函数,拿到状态
- CacheCheckResult status = CheckCacheAll(time, high, low);
-
- switch(status)
- {
- // --- 缓存失效:全量重算 ---
- case CACHE_INVALID:
- FullMerge(high, low, time);
- Print("执行全量合并,完成后组数=", ArraySize(g_mergeArray));
- break;
-
- // --- 缓存有效:找到断点,增量续算 ---
- case CACHE_CONTINUE:
- {
- // 找到缓存最后一组对应的K线下标
- datetime lastCacheTime = g_mergeArray[ArraySize(g_mergeArray)-1].time;
- int breakIdx = -1;
-
- // 从后往前找,匹配更快
- for(int i = rates_total - 1; i >= 0; i--)
- {
- if(time[i] == lastCacheTime)
- {
- breakIdx = i;
- break;
- }
- }
-
- // 从下一根K线开始增量合并
- if(breakIdx >= 0)
- {
- IncrementMerge(high, low, time);
-
- Print("执行增量续算,完成后组数=", ArraySize(g_mergeArray));
- break;
-
- }
- else
- {
- // 兜底:找不到断点就全量重算
- FullMerge(high, low, time);
- }
- break;
- }
-
- // 加载不完全,清空已有的旧合并方框,避免显示错位
- case CACHE_UNFINISHED:
- if(ShowMergeBox)
- {
- int objTotal = ObjectsTotal(0);
- for(int i = objTotal - 1; i >= 0; i--)
- {
- string objName = ObjectName(0, i);
- if(StringFind(objName, "merge_box_", 0) == 0)
- {
- ObjectDelete(0, objName);
- }
- }
- }
- // 终端日志打印提示,用户可通过日志排查
- Print("历史K线加载未完成,合并方框暂不显示,请等待数据加载完成或刷新当前图表");
-
- break;
-
- }
- }
- // ==================================================
- // 分支2:正常运行(prev_calculated > 0)
- // 触发场景:日常行情推进,只有末尾新增K线
- // ==================================================
- else
- {
- // 只处理新增K线,性能最优
- IncrementMerge(high, low, time);
-
-
- // ===== 插入调试日志 =====
- Print("【调试】正常运行增量合并,完成后组数=", ArraySize(g_mergeArray));
- // ========================
-
-
- }
-
- // ==================================================
- // 最后一步:绘制合并方框
- // ==================================================
- /*
- // ===== 插入调试日志 =====
- Print("【调试】进入绘图环节 ShowMergeBox=", ShowMergeBox);
- // ========================
- */
-
- if(ShowMergeBox)
- {
- int cacheSize = ArraySize(g_mergeArray);
- // 双重校验:有有效数据 且 最后一组时间在当前K线范围内,才绘图
- if(cacheSize >= 2)
- {
-
- DrawMergeBoxes(time);
-
- /*
- // ===== 插入调试日志 =====
- Print("【调试】绘图函数调用完成");
- // ========================
- */
-
- }
- }
-
- // 标准返回,系统会记录为下一次的prev_calculated
- return rates_total;
- }
-
- void OnDeinit(const int reason)
- {
-
-
- // 【最优先】先保存最新数据,再做其他清理
- if(ArraySize(g_mergeArray) >= 2)
- {
- SaveMergeCache();
- Print("数据保存函数调用成功!");
- }
-
-
- }
-
-
-
复制代码
二、下面是全局声明defines部分的代码
- //+------------------------------------------------------------------+
- //| 缠论指标公共数据定义文件 chanlun_defines.mqh |
- //| Copyright 2026,ZHAO. |
- //| qq317008881 |
- //+------------------------------------------------------------------+
- #property copyright "Copyright 2026,ZHAO."
- #property link "qq317008881"
-
- #ifndef CHANLUN_DEFINES_H
- #define CHANLUN_DEFINES_H
-
- // 合并K线单元缓存数据结构体
- struct MergedKLine
- {
- datetime time; // 该合并组最新一根原始K线的开盘时间(唯一标识,用于断点校验)
- double high; // 合并后的高点
- double low; // 合并后的低点
- int startIdx; // 对应原始K线的起始索引(最老一根,可选,方便回溯)
- int endIdx; // 对应原始K线的结束索引(最新一根,断点用)
- int trend; // 即合并方向,新k线组第一根k线与它前一根k线判断
- }; // 合并单元结构体有六个参数
-
- //+------------------------------------------------------------------+
- //| 全局状态与缓存 |
- //+------------------------------------------------------------------+
- MergedKLine g_mergeArray[]; // 内存缓存:存放合并后的K线组合,是一个结构体数组
-
- //+------------------------------------------------------------------+
- //| 合并k线方框显示开关 |
- //+------------------------------------------------------------------+
- input bool ShowMergeBox = true; // 显示合并K线方框
-
-
-
- #endif
-
复制代码
三、下面是合并函数merge部分的代码
- //+------------------------------------------------------------------+
- //| 包含K线合并函数 fun_chanlun_merge.mqh |
- //| Copyright 2026,ZHAO. |
- //| qq317008881 |
- //+------------------------------------------------------------------+
- #property copyright "Copyright 2026,ZHAO."
- #property link "qq317008881"
- #ifndef FUN_CHANLUN_MERGE_H
- #define FUN_CHANLUN_MERGE_H
-
- #include "chanlun_defines.mqh"
-
- //+------------------------------------------------------------------+
- //| 函数实现 |
- //+------------------------------------------------------------------+
- //*****************************************************************************************************************************
- //壹.检查两个K线是否满足包含关系的函数,true为符合,false为不符合
- //highNew/lowNew,当前新K线(要比较的)高价/低价,highOld/lowOld,此前旧K线(被比较的)高价/低价,
- bool CheckMergeStatus(double highNew, double lowNew,
- double highOld, double lowOld) //默认新高、新低、旧高、旧低,防止后续传参错误
- {
- // 逻辑表达式本身可以得到布尔值,直接返回
- return (highNew >= highOld && lowNew <= lowOld)
- || (highOld >= highNew && lowOld <= lowNew);
- }
- //*****************************************************************************************************************************
- //贰.检查K线合并方向,1为向上合并,-1为向下合并
- //highNew/lowNew,此前较新K线(要比较的)高价/低价,highOld/lowOld,此前第二根K线(更老的,被比较的)高价/低价,
- int CheckMergeTrend(double highNew, double lowNew,
- double highOld, double lowOld) //默认新高、新低、旧高、旧低,防止后续传参错误
- {
- bool isUp = (highNew > highOld) && (lowNew > lowOld); //高高向上
- bool isDown = (highNew < highOld) && (lowNew < lowOld); //低低向下
-
- if(isUp == true)
- {
- return 1;
- }
- if(isDown == true)
- {
- return -1;
- }
- else
- {
- return 0; // 输入不合法:两根K线存在包含或交叉
- }
- }
- //*****************************************************************************************************************************
- //叁.全量合并函数
- void FullMerge(const double &high[], //引入K线最高价数组
- const double &low[], //引入K线最低价数组
- const datetime &time[]) //引入K线开盘时间数组
- {
- // 异常兜底:K线数量不足3根时无法启动合并,直接返回
- // high:数组high[],ArraySize:求数组元素个数,总K线数<3个
- int ratesTotal = ArraySize(high);
- if(ratesTotal < 3 )
- {
- ArrayResize(g_mergeArray, 0); //ArrayResize:数组修改函数,把g_mergeArray数组归零
- return;
- }
-
- // --------------------------
- // 1. 初始化状态,即默认先输入两个K线作为合并基础
- // --------------------------
- ArrayResize(g_mergeArray, 0); // 把g_mergeArray数组归零,清空历史结果
-
- // 第一个K线组(第一根K线):索引0,直接存入结果(作为方向基准的左锚点)
- MergedKLine Group0; // 声明一个结构体变量Group0,即第一组K线组,沿用MQL5编号习惯,从0开始编号
- Group0.time = time[0]; // 下标0就是第一根K线,Group0开盘时间即是0号K线开盘时间
- Group0.high = high[0]; // Group0最高价就是0号K线最高价
- Group0.low = low[0]; // Group0最低价就是0号K线最低价
- Group0.startIdx = 0; // Group0中只有一个k线,起止标号都是0号K线标号
- Group0.endIdx = 0; // Group0中只有一个k线,起止标号都是0号K线标号
- Group0.trend = 0; // Group0是第一组k线,没有合并方向
-
- ArrayResize(g_mergeArray, 1); //把数组g_mergeArray长度变成1,即增加一个元素
- g_mergeArray[0] = Group0; //把0号k线的数据赋值给数组g_mergeArray中下标0的元素
-
- //第二个K线组:索引1,与索引0一样,直接存入结果
- MergedKLine Group1;
- Group1.time = time[1]; // 下标1就是第二根K线
- Group1.high = high[1];
- Group1.low = low[1];
- Group1.startIdx = 1;
- Group1.endIdx = 1;
- //先默认前两根K线不合并,作为后续可能合并的方向条件
- //先判断第一组合并方向
- int Trend = CheckMergeTrend(high[1], low[1], high[0], low[0]); //保持先新后旧的排序,
- //只在这里做一次兜底:初始方向不明时,默认按向上处理
- if(Trend == 0) Trend = 1;
- Group1.trend = Trend; // Group1是第二组k线,可以与此一组k线判断方向了
-
- ArrayResize(g_mergeArray, 2); //把数组g_mergeArray长度变成2
- g_mergeArray[1] = Group1; //把1号k线的数据赋值给数组g_mergeArray中下标1的元素
-
- //合并主循环,从第三根K线即下标2的K线开始,一直到最新完成的k线(即总K线数减2)结束
- for(int i = 2; i <= (ratesTotal-2); i++)
- {
- int GroupLastIdx = ArraySize(g_mergeArray) - 1; //得到数组g_mergeArray里最后一个(第Last个)元素的下标
- double GroupHighLast = g_mergeArray[GroupLastIdx].high; //得到数组g_mergeArray里最后一个(第Last个)元素的高价
- double GroupLowLast = g_mergeArray[GroupLastIdx].low; //得到数组g_mergeArray里最后一个(第Last个)元素的低价
- int GroupTrendLast = g_mergeArray[GroupLastIdx].trend; // 直接从最后一组取当前合并方向
-
- //如果符合合并关系,K2(Ki)会合并到K1(GroupLast)所在的K线组中去,需要修改当前k线组的最新K线开盘时间、最高价、最低价、结束k线下标
- if(CheckMergeStatus(high[i], low[i], GroupHighLast, GroupLowLast) == true) //调用是否符合合并关系函数进行判断
- {
- //情况1:向上合并
- if(GroupTrendLast == 1)
- {
- g_mergeArray[GroupLastIdx].high = MathMax(GroupHighLast, high[i]); //高价取两个K线高价中的较高者
- g_mergeArray[GroupLastIdx].low = MathMax(GroupLowLast, low[i]); //低价取两个K线低价中的较高者
- }
- //情况2:向下合并
- else
- {
- g_mergeArray[GroupLastIdx].high = MathMin(GroupHighLast, high[i]); //高价取两个K线高价中的较低者
- g_mergeArray[GroupLastIdx].low = MathMin(GroupLowLast, low[i]); //低价取两个K线低价中的较低者
- }
-
- g_mergeArray[GroupLastIdx].endIdx = i; // 更新结束K线索引,开始K线索引不需要更新
- g_mergeArray[GroupLastIdx].time = time[i]; // 更新组时间为最新K线时间
-
- }
- else // --- 不包含:当前K线做成新组合,追加到数组末尾 ---
- {
- MergedKLine GroupNew; // 声明一个结构体变量GroupNew,即新K线组
- GroupNew.time = time[i]; // 把新k线i赋值给结构体变量GroupNew
- GroupNew.high = high[i];
- GroupNew.low = low[i];
- GroupNew.startIdx = i;
- GroupNew.endIdx = i;
- //确认新K线与旧k线组不能合并,二者判断后续可能合并的方向条件
- int newTrend = CheckMergeTrend(high[i], low[i], GroupHighLast, GroupLowLast); //保持先新后旧的排序,
- GroupNew.trend = newTrend; // GroupNew.是新k线组,获得后续合并方向
-
- int len = ArraySize(g_mergeArray); //在循环体内,每次循环的数组数量不固定,要用变量,后续每次循环到此都会加1
- ArrayResize(g_mergeArray, len + 1); //给g_mergeArray扩容,加一个元素
- g_mergeArray[len] = GroupNew; //把新K线组GroupNew的数据赋值给g_mergeArray
-
- }
-
- }
-
- }
- //*****************************************************************************************************************************
- //肆.增量合并函数
- void IncrementMerge(const double &high[], //输入参数与全量合并函数一样,
- const double &low[],
- const datetime &time[])
- {
- // --------------------------
- // 1. 基础数值计算
- // --------------------------
- int ratesTotal = ArraySize(high); // 内部自行获取当前加载总K线数量
- int endIdx = ratesTotal - 2; // 最新闭合K线索引
- int arraySize = ArraySize(g_mergeArray);
- // --------------------------
- // 2. 边界兜底检查
- // --------------------------
- // 兜底1:合并数组不足2组,说明未初始化,自动执行全量合并
- if(arraySize < 2)
- {
- FullMerge(high, low, time);
- return;
- }
- // 兜底2:K线数量不足3根,无合并意义,直接退出
- if(ratesTotal < 3) return;
-
- // --------------------------
- // 3. 计算增量遍历起点
- // --------------------------
- int GroupLastIdx = arraySize - 1; //得到数组g_mergeArray里最后一个(第Last个)元素的下标
- int EndLastIdx = g_mergeArray[GroupLastIdx].endIdx; // 最后一个元素的最后一根原始K线下标
- int GroupNewIdx = EndLastIdx + 1; // 本次从下一根开始处理
-
- // 没有新的闭合K线,无需处理
- //GroupNewIdx需要处理的k线下标,ratesTotal-2 最新完成的k线下标
- //如果需要处理的k线比最新完成的K线还新,那它就是最新未完成的k线,不处理
- if(GroupNewIdx > (ratesTotal-2)) return;
-
- // --------------------------
- // 4. 增量主循环(与全量循环逻辑完全一致)
- // --------------------------
-
- for(int i = GroupNewIdx; i <= (ratesTotal-2); i++) //循环开始k线是K线组最后一根k线的下一根k线,循环结束k线是最新一根完成k线
- {
- // 取出当前最后一个合并组的高低点及合并方向作为基准
- int GroupLastIdx = ArraySize(g_mergeArray) - 1; //得到数组g_mergeArray里最后一个(第Last个)元素的下标
- double GroupHighLast = g_mergeArray[GroupLastIdx].high; //得到数组g_mergeArray里最后一个(第Last个)元素的高价
- double GroupLowLast = g_mergeArray[GroupLastIdx].low; //得到数组g_mergeArray里最后一个(第Last个)元素的低价
- int GroupTrendLast = g_mergeArray[GroupLastIdx].trend; // 直接从最后一组取当前合并方向
-
- if(CheckMergeStatus(high[i], low[i], GroupHighLast, GroupLowLast) == true) // 调用判断包含关系函数
- {
- // 包含:更新最后一个合并组的高低点与结束位置
- if(GroupTrendLast == 1) // 向上合并:取更高的高点、更高的低点
- {
- g_mergeArray[GroupLastIdx].high = MathMax(GroupHighLast, high[i]);
- g_mergeArray[GroupLastIdx].low = MathMax(GroupLowLast, low[i]);
- }
- else // 向下合并:取更低的高点、更低的低点
- {
- g_mergeArray[GroupLastIdx].high = MathMin(GroupHighLast, high[i]);
- g_mergeArray[GroupLastIdx].low = MathMin(GroupLowLast, low[i]);
- }
- g_mergeArray[GroupLastIdx].endIdx = i;
- g_mergeArray[GroupLastIdx].time = time[i];
- }
- else
- {
- // 不包含:追加新的独立合并组
- MergedKLine GroupNew; // 声明一个结构体变量GroupNew,即新K线组
- GroupNew.time = time[i]; // 把新k线i赋值给结构体变量GroupNew
- GroupNew.high = high[i];
- GroupNew.low = low[i];
- GroupNew.startIdx = i;
- GroupNew.endIdx = i;
- //确认新K线与旧k线组不能合并,二者判断后续可能合并的方向条件
- int newTrend = CheckMergeTrend(high[i], low[i], GroupHighLast, GroupLowLast); //保持先新后旧的排序,
- GroupNew.trend = newTrend; // GroupNew.是新k线组,获得后续合并方向
-
- int len = ArraySize(g_mergeArray); //g_mergeArray第一次循环到此已有2个元素,后续每次循环到此都会加1
- ArrayResize(g_mergeArray, len + 1); //给g_mergeArray扩容,加一个元素
- g_mergeArray[len] = GroupNew; //把新K线组GroupNew的数据赋值给g_mergeArray
-
- }
- }
-
- }
-
- #endif
-
-
-
-
- /*
-
- 缠论中对"包含K线"的定义:
-
- 1.两根相邻 K 线中,如果一根 K 线的最高价 ≥ 另一根的最高价,且最低价 ≤ 另一根的最低价,
- 也就是一根 K 线的高低点完全包裹住另一根,就称这两根 K 线存在「包含关系」。
-
- 2.缠论中对包含处理顺序的规定:从左到右,依次合并。必须按照 K 线出现的时间顺序,从最老
- 到最新(从左到右)依次处理,先合并前面的,再用合并后的新 K 线和后面的 K 线比较,不能
- 跳着处理、不能从右往左处理。
-
- 3.缠论中对包含处理合并方向的规定:不是看包含的两根 K 线谁高谁低,而是由「这组包含关系之
- 前,最近一根没有包含关系的 K 线」来决定:取包含组左侧最近的一根独立 K 线,和包含组的第
- 一根 K 线比较:
- (1)如果独立 K 线高点 < 包含组第一根的高点,属于上升趋势中的包含,按向上合并处理;
- (2)如果独立 K 线高点 > 包含组第一根的高点,属于下降趋势中的包含,按向下合并处理;
- (3)非包含关系的两根k线,如果一个高点高于另一个的,那么低点也一定高于另一个的,反之同
- 理,代码中同时比较傲低点,也是为了增加容错率。
-
- 4.缠论中对合并取值的规定:
- (1)向上合并(高高原则):合并后的新 K 线,高点取两根 K 线高点的最大值,低点取两根 K 线
- 低点的最大值。也就是:`新高 = max(高1, 高2)`,`新低 = max(低1, 低2)`;
- (2)向下合并(低低原则):合并后的新 K 线,高点取两根 K 线高点的最小值,低点取两根 K 线
- 低点的最小值。
-
- 5.缠论中对于连续包含的处理规定:如果出现三根及以上连续的包含 K 线,就从左到右滚动合并:
- (1)先合并前两根,得到一个新的 K 线组;
- (2)用这根新 K 线和第三根比较,如果还是包含关系,继续按同一个方向合并;
- (3)直到遇到不包含的 K 线,输出最终合并后的 K 线,继续往后遍历。
- (4)连续包含的方向,由第一组包含的前序方向决定,中间不会变方向。
-
- 6.关于起始边界的处理:图表最开头的前两根 K 线怎么弄,禅师没说。我们可以给出最简单的处理方法,
- 把这两个K线当作独立k线组,由它们推导出第三根可能合并的方向,如果二者存在合并关系,那就默
- 认第三根k线要合并的话就按多头方向合并。有人说缠论是从头推理,开头错了就都错了,实际上只要
- 出现连续的三根不包含的k线,那它们前面合并方向是向上还是向下都没意义了,我管这个叫方向阻断。
- 实际操作中往往要算几千几万根k线,存在大量的单边行情,也就是方向阻断效应,头几根丢掉也无所
- 谓,没什么影响。
-
- 7.补充规定:缠论中并没有特殊要求,呈现包含关系的两个K线是左边的长一点,还是右边的长一点,所以
- 我们默认无论是左K包右K,还是右K包左K,都是包含关系,都按上面的要求处理。
-
- 8.显示要求:这一分析步骤,在代码中最终只形成了一个处理后的K线数据组,我们是看不到的。为了让我
- 们可以看到代码分析的成果,评判它是否正确,需要进行可视化处理。简单来说,把向上合并的K线组用
- 蓝色方框框起来,向下合并的K线组用红色方框框起来。
-
- 9.记录要求:程序中生成的k线数组是放在内存缓存中的,关闭程序后就被清理,下次打开又重新计算一遍,
- 为了避免重复计算浪费性能,同时也是给程序存档,我们需要设计硬盘缓存功能,数据以二进制存入硬盘,
- 同时还需要另设计一个脚本一建转化该文档,方便用户阅读。
-
- */
复制代码
四、下面是绘图函数draw merge部分的代码
- //+------------------------------------------------------------------+
- //| draw_chanlun_merge.mqh |
- //| Copyright 2026,ZHAO. |
- //| qq317008881 |
- //+------------------------------------------------------------------+
- #property copyright "Copyright 2026,ZHAO."
- #property link "qq317008881"
- #ifndef DRAW_CHANLUN_MERGE_H
- #define DRAW_CHANLUN_MERGE_H
-
- #include "chanlun_defines.mqh"
-
- //+------------------------------------------------------------------+
- //| 在主图绘制合并K线组的方框 |
- //| 参数:time[] K线时间数组 |
- //+------------------------------------------------------------------+
- void DrawMergeBoxes(const datetime &time[]) //DrawMergeBoxes:绘制合并方框,自定义函数
- {
- // ========== 1. 先清空所有旧的合并方框,避免残留 ==========
- // 倒序遍历主图所有对象,删除我们生成的方框(避免正序删除导致的索引错位)
- //`ObjectsTotal` 内置图表图形对象统计函数,0代表默认当前图表,返回一个整数,代表图表中符合条件的图形对象总个数。
- // 识别我们自己画的对象:以 merge_box_ 开头
- int delCount = 0;
- int total = ObjectsTotal(0);
- for(int i = total-1; i >= 0; i--)
- {
- string name = ObjectName(0, i);
- if(StringFind(name, "merge_box_", 0) == 0)
- {
- ObjectDelete(0, name);//`ObjectDelete` 官方内置图形对象删除函数
- delCount++;
- }
- }
-
- Print("【调试】清空旧方框数量=", delCount);
-
- int arraySize = ArraySize(g_mergeArray);
- //兜底设置,如果g_mergeArray里面一个成员都没有,那就直接返回
- if(arraySize < 1) return;
-
- int ratesTotalT = ArraySize(time);
- // ========== 2. 遍历每个合并组,逐个绘制方框 ==========
- for(int i = 0; i < arraySize; i++)
- {
- // 边界兜底:索引非法则跳过
- if(g_mergeArray[i].startIdx < 0 ||
- g_mergeArray[i].endIdx >= ratesTotalT ||
- g_mergeArray[i].startIdx > g_mergeArray[i].endIdx)
- continue;
-
- //`IntegerToString` 方内置的整型转字符串函数,把数组中的序号i转化为对应数字,相当于给方框按照数据成员序列编号
- string boxName = "merge_box_" + IntegerToString(i);
-
- // 计算两个对角坐标,即起止k线的时间和价格参数
- datetime leftTime = time[g_mergeArray[i].startIdx];
- datetime rightTime = time[g_mergeArray[i].endIdx];
- double topPrice = g_mergeArray[i].high;
- double bottomPrice = g_mergeArray[i].low;
-
- // 直接传入两个对角的时间+价格,一步定位
- //`ObjectCreate` 图形对象创建函数,是所有自定义绘图的核心入口,矩形、趋势线、文字、箭头等都靠它创建。
- //返回值是布尔值,代表是否创建成功,如果创建不成功,跳过,进行下一个
- if(!ObjectCreate(0, boxName, OBJ_RECTANGLE, 0, leftTime, topPrice, rightTime, bottomPrice))
- {
- continue;
- }
-
- // --- 设置颜色和样式 ---
- color borderColor;
- if(g_mergeArray[i].trend == 1) borderColor = clrBlue; // 向上合并 → 蓝色
- else if(g_mergeArray[i].trend == -1) borderColor = clrRed; // 向下合并 → 红色
- else borderColor = clrGray; // 初始锚点组 → 灰色
-
- //`ObjectSetInteger` 官方图表对象体系中的整数型属性设置函数,专门用于给已创建的图形对象设置整数类型的属性(颜色、线宽、线条样式、布尔开关等)。
- //其第三个参数属性枚举值,指定要修改对象的哪一项属性,是最核心的参数。
- ObjectSetInteger(0, boxName, OBJPROP_COLOR, borderColor); // 边框颜色
- ObjectSetInteger(0, boxName, OBJPROP_STYLE, STYLE_SOLID); // 实线边框
- ObjectSetInteger(0, boxName, OBJPROP_WIDTH, 1); // 边框宽度1
- ObjectSetInteger(0, boxName, OBJPROP_FILL, true); // 填充(透明)
- ObjectSetInteger(0, boxName, OBJPROP_BACK, false); // 显示在K线前方
- ObjectSetInteger(0, boxName, OBJPROP_SELECTABLE, false); // 禁止鼠标选中,避免误操作
- }
-
-
-
- }
-
-
- #endif
复制代码
五、下面是缓存部分cache部分的代码
- //+------------------------------------------------------------------+
- //| 包含K线合并函数 fun_chanlun_cache.mqh |
- //| Copyright 2026,ZHAO. |
- //| qq317008881 |
- //+------------------------------------------------------------------+
- #property copyright "Copyright 2026,ZHAO."
- #property link "qq317008881"
- #ifndef FUN_CHANLUN_CACHE_H
- #define FUN_CHANLUN_CACHE_H
-
- #include "chanlun_defines.mqh"
-
- //+------------------------------------------------------------------+
- //| 函数实现 |
- //+------------------------------------------------------------------+
- //*****************************************************************************************************************************
- //+------------------------------------------------------------------+
- //| 壹、时间周期显示转换函数 |
- //| 把Period()函数默认返回的数值转化为具体文字,方便阅读 |
- //+------------------------------------------------------------------+
- string PeriodToString(ENUM_TIMEFRAMES period)
- {
- switch(period)
- {
- case PERIOD_M1: return "M1";
- case PERIOD_M5: return "M5";
- case PERIOD_M15: return "M15";
- case PERIOD_M30: return "M30";
- case PERIOD_H1: return "H1";
- case PERIOD_H4: return "H4";
- case PERIOD_D1: return "D1";
- case PERIOD_W1: return "W1";
- case PERIOD_MN1: return "MN1";
- default: return IntegerToString((int)period); // 未知周期兜底用数字
- }
- }
- //*****************************************************************************************************************************
- //+------------------------------------------------------------------+
- //| 贰、保存合并缓存到硬盘文件 |
- //| 返回值:true=保存成功,false=保存失败 |
- //+------------------------------------------------------------------+
- #define MERGE_CACHE_MAGIC 0x434C4D47 //缓存身份标记
- #define MERGE_CACHE_VERSION 1 //缓存版本标记
-
- bool SaveMergeCache()
- {
- // 文件名自动拼接品种+周期,不同品种/周期的缓存互不干扰
- //StringFormat:内置系统函数,按照指定的格式模板,把多种不同类型的数据(整数、小数、字符串、时间等)拼接成一个规整的字符串。
- string fileName = StringFormat("merge_cache_%s_%s.bin", Symbol(), PeriodToString(Period()));
-
- // 以二进制写入模式打开文件,覆盖旧文件,`FileOpen` 是 MQL5 内置的**核心文件操作入口函数**,返回一个正整数,代表文件句柄
- int fileHandle = FileOpen(fileName, FILE_BIN | FILE_WRITE );
- if(fileHandle == INVALID_HANDLE)
- {
- Print("保存合并缓存失败,错误码:", GetLastError());
- return false;
- }
-
- // 缓存文件身份标识:魔数 + 版本
- FileWriteInteger(fileHandle, MERGE_CACHE_MAGIC);
- FileWriteInteger(fileHandle, MERGE_CACHE_VERSION);
- // 第1步:先写入数组总长度(读取时先知道要开多大空间)
- int arraySize = ArraySize(g_mergeArray);
- FileWriteInteger(fileHandle, arraySize);
-
- // 第2步:逐个写入每个合并组的字段,顺序必须和结构体完全对应
- for(int i = 0; i < arraySize; i++)
- {
- FileWriteLong (fileHandle, g_mergeArray[i].time); // datetime 本质是长整型
- FileWriteDouble (fileHandle, g_mergeArray[i].high);
- FileWriteDouble (fileHandle, g_mergeArray[i].low);
- FileWriteInteger(fileHandle, g_mergeArray[i].startIdx);
- FileWriteInteger(fileHandle, g_mergeArray[i].endIdx);
- FileWriteInteger(fileHandle, g_mergeArray[i].trend); // 同步保存方向字段
- }
-
- FileFlush(fileHandle);
- FileClose(fileHandle);
- return true;
- }
- //*****************************************************************************************************************************
- //+------------------------------------------------------------------+
- //| 叁、从硬盘加载二进制合并缓存到内存数组 |
- //| 返回值:true=加载成功,false=无缓存/缓存损坏/读取失败 |
- //+------------------------------------------------------------------+
- bool LoadMergeCache()
- {
- // 生成与保存规则完全一致的文件名(品种+可读周期)
- string fileName = StringFormat("merge_cache_%s_%s.bin", Symbol(), PeriodToString(Period()));
-
- // 以二进制只读模式打开文件
- int fileHandle = FileOpen(fileName, FILE_BIN | FILE_READ);
- if(fileHandle == INVALID_HANDLE)
- {
- // 文件不存在属于正常情况(首次运行),不算错误
- return false;
- }
-
- // 头部校验
- int magic = FileReadInteger(fileHandle);
- int version = FileReadInteger(fileHandle);
- if(magic != MERGE_CACHE_MAGIC || version != MERGE_CACHE_VERSION)
- {
- FileClose(fileHandle);
- return false;
- }
-
- // 数据合法性校验:不足2组无意义,直接丢弃缓存
- // FileReadInteger:读取函数,返回读取到的元素个数
- int fileCount = FileReadInteger(fileHandle);
- if(fileCount < 2)
- {
- FileClose(fileHandle);
- return false;
- }
-
- // 调整全局数组合适大小,准备接收数据,如果加载不成功返回失败
- if(ArrayResize(g_mergeArray, fileCount) != fileCount)
- {
- ArrayResize(g_mergeArray, 0);
- FileClose(fileHandle);
- return false;
- }
-
- // 读取顺序必须和保存函数的写入顺序完全一致
- for(int i = 0; i < fileCount; i++)
- {
- g_mergeArray[i].time = (datetime)FileReadLong(fileHandle); // 时间(长整型转datetime)
- g_mergeArray[i].high = FileReadDouble(fileHandle); // 高点
- g_mergeArray[i].low = FileReadDouble(fileHandle); // 低点
- g_mergeArray[i].startIdx = FileReadInteger(fileHandle); // 起始K线索引
- g_mergeArray[i].endIdx = FileReadInteger(fileHandle); // 结束K线索引
- g_mergeArray[i].trend = FileReadInteger(fileHandle); // 相对前一组的合并方向
-
- // 字段合法性校验
- if(g_mergeArray[i].high < g_mergeArray[i].low ||
- g_mergeArray[i].startIdx < 0 ||
- g_mergeArray[i].startIdx > g_mergeArray[i].endIdx ||
- (g_mergeArray[i].trend != -1 &&
- g_mergeArray[i].trend != 0 &&
- g_mergeArray[i].trend != 1))
- {
- Print("缓存字段损坏,i=", i);
- ArrayResize(g_mergeArray, 0);
- FileClose(fileHandle);
- return false;
- }
- }
-
- // 完整性校验
- if(FileTell(fileHandle) != FileSize(fileHandle))
- {
- Print("缓存文件长度不匹配,可能被截断");
- ArrayResize(g_mergeArray, 0);
- FileClose(fileHandle);
- return false;
- }
-
- // 关闭文件,释放系统资源
- FileClose(fileHandle);
-
- Print("历史合并缓存加载成功,共", fileCount, "个合并组");
- return true;
- }
- //*****************************************************************************************************************************
- //| 缓存校验结果状态,这是一个枚举
- enum CacheCheckResult
- {
- CACHE_INVALID, // INVALID:无效的。缓存失效,需要全量重算(对应:损坏/扩充/截断/报价修正)
- CACHE_CONTINUE, // CONTINUE:继续。缓存有效,可正常增量续算(对应:正常顺延)
- CACHE_UNFINISHED // UNFINISHED:未完成的。K线加载不全,暂用现有缓存(对应:数据加载不全)
- };
-
- //+--------------------------------------------------------------------------------------------------------+
- //| 统一缓存校验函数:整合正常顺延、当前数据家在不全、历史数据扩充、历史报价静默修正、缓存文件损坏等5项检查
- //| CacheCheckResult:自定义返回状态枚举,返回值:缓存状态枚举
- //+--------------------------------------------------------------------------------------------------------+
- CacheCheckResult CheckCacheAll(const datetime &time[],
- const double &high[],
- const double &low[])
- {
- Print("开始缓存校验!");
-
- int arraySize = ArraySize(g_mergeArray);// 获取缓存数组的长度
- int ratesTotalT = ArraySize(time);
- int ratesTotalH = ArraySize(high);
- int ratesTotalL = ArraySize(low);
-
- // 0. 基础数组兜底(防止越界)
- if(ratesTotalT < 2 || ratesTotalH < 2 || ratesTotalL < 2)
- {
- Print("当前加载K线数量小于2个,请检查MT5网络状态并刷新图表界面!");
- return CACHE_UNFINISHED; //如果K线数据少于2个,未加载完成
- }
-
-
- if(arraySize < 2)
- {
- Print("缓存数据少于2个,缓存无效!");
- return CACHE_INVALID; //如果缓存数据少于2个,缓存无效
- }
-
-
- // 1. 缓存损坏校验
- if(g_mergeArray[0].high <= g_mergeArray[0].low)
- {
- Print("缓存损坏!");
- return CACHE_INVALID; //如果0号缓存数据高点低于或等于低点,缓存已损坏,无效
- }
-
- if(g_mergeArray[arraySize-1].high <=
- g_mergeArray[arraySize-1].low)
- {
- Print("缓存损坏!");
- return CACHE_INVALID; //如果最大下标号缓存数据高点低于或等于低点,缓存已损坏,无效
- }
-
- if(g_mergeArray[0].time >=
- g_mergeArray[arraySize-1].time)
- {
- Print("缓存损坏!");
- return CACHE_INVALID; //如果0号缓存数据时间晚于(即大于或等于)最大下标号,缓存已损坏,无效
- }
-
- if(g_mergeArray[arraySize-1].startIdx < 0)
- {
- Print("缓存损坏!");
- return CACHE_INVALID; //如果最后一组缓存数据起始下标小于零,缓存已损坏,无效
- }
-
- if(g_mergeArray[arraySize-1].startIdx >
- g_mergeArray[arraySize-1].endIdx)
- {
- Print("缓存损坏!");
- return CACHE_INVALID; //如果最后一组缓存数据起始下标大于结束下标,缓存已损坏,无效
- }
-
- // 2. 历史扩充/截断校验
- // 第一组永远是单K,endTime就是缓存最早时间
- if(time[0] <
- (g_mergeArray[0].time))
- {
- Print("加载了更早的k线,重新计算!");
- return CACHE_INVALID; // 如果本次加载0号K线时间早于(即小于)缓存数据0号时间,加载了更早历史,要重新计算缓存
- }
-
- if(time[0] >
- (g_mergeArray[0].time))
- {
- Print("加载历史k线被截断,重新计算!");
- return CACHE_INVALID; // 如果本次加载0号K线时间晚于(即大于)缓存数据0号时间,老K线被截断,要重新计算缓存
- }
-
- // 3. 加载不全校验
- if((time[ratesTotalT - 1]) <
- (g_mergeArray[arraySize-1].time))
- {
- Print("加载k线不全,请检查MT5网络状态并刷新图表界面!");
- return CACHE_UNFINISHED;// 如果本次加载最大号k线时间早于(即小于)缓存数据最大号时间,加载不完全,
- }
- // 4. 报价静默修正校验,即检查当前加载k线价格与缓存中的价格是否一致
- int lastStart = g_mergeArray[arraySize-1].startIdx; //获得最后一组的起始k线下标
- int lastEnd = g_mergeArray[arraySize-1].endIdx; //获得最后一组的结束k线下标
-
- if(lastEnd >= ratesTotalT)
- {
- Print("历史K线可能被修正,重新计算!");
- return CACHE_INVALID; //如果最后一组的结束k线下标大于或等于总k线数,说明旧k线被删除过,缓存作废
- }
-
- if(g_mergeArray[arraySize-1].time != time[lastEnd])
- {
- Print("历史K线可能被修正,重新计算!");
- return CACHE_INVALID;//如果最后一组的结束k线时间不等于相应k线时间按,说明旧k线调整过,缓存作废
- }
- // 重算当前K线中,这段区间的真实高低点
- double curHigh = high[g_mergeArray[arraySize - 1].startIdx]; //定义一个变量curHigh等于缓存中最后一个数组的起始k线的最高价
- double curLow = low[g_mergeArray[arraySize - 1].startIdx]; //定义一个变量curLow等于缓存中最后一个数组的起始k线的最低价
- for(int i = lastStart + 1; i <= lastEnd; i++) //从后一个k线开始到缓存数组结束k线为止
- {
- if(high[i] > curHigh) curHigh = high[i];//如果后面的K线价更高,就更新curHigh
- if(low[i] < curLow) curLow = low[i]; //如果后面的K线价更低,就更新curLow
- }
-
- // 和缓存记录的组合高低点对比,超出精度就判定失效
- if(fabs(curHigh - g_mergeArray[arraySize - 1].high) >
- SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE)) //SymbolInfoDouble系统自带求最小点数的函数
- {
- Print("历史K线可能被修正,重新计算!");
- return CACHE_INVALID;
- }
- if(fabs(curLow - g_mergeArray[(arraySize - 1)].low) >
- SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE))
- {
- Print("历史K线可能被修正,重新计算!");
- return CACHE_INVALID;
- }
- // 全部通过
- Print("缓存校验通过!");
- return CACHE_CONTINUE;
- }
-
- //*****************************************************************************************************************************
- //+------------------------------------------------------------------+
- //|伍、清空内存中的合并数组,释放内存 |
- //+------------------------------------------------------------------+
- void ClearMergeArray()
- {
- ArrayResize(g_mergeArray, 0); // 数组大小设为0,内存直接释放
- }
-
-
- #endif
复制代码
目前完成的功能只是按缠论第一步,将有合并关系的k线合并起来,供后续顶底分型使用。绘图只是为了形象显示合并结果,以检查程序合并的是否正确。具体效果如下图所示:
缠论是从前往后合并,并且有合并方向,通过红蓝色框很容易分析程序合并的对不对、有没有错漏,目前来看,我觉得还是很靠谱的。 |