【缠论】用豆包从零开始搓缠论

| 发表于 前天 11:08 | 显示全部楼层 |复制链接
之前试水用豆包写过一段合并K线的代码,但是因为豆包搞反了MT5原始数组的数据下标方向,导致最终的合并是从后往前合并的。后来我干脆用工程方法重新来过,把代码拆成全局声明、合并函数、缓存函数、绘图函数五个模块,算上主程序,共五个模块,重新写了一遍。现在把它们贴出来请大家斧正。因为也是现学现卖,就免费给大家研究。
一、下面是mq5主入口程序代码:
  1. //+------------------------------------------------------------------+
  2. //|                                         论指标主入口 chanlun.mq5 |
  3. //|                                             Copyright 2026,ZHAO. |
  4. //|                                                      qq317008881 |
  5. //+------------------------------------------------------------------+
  6. #property copyright "Copyright 2026,ZHAO."
  7. #property link      "qq317008881"
  8. #property version   "1.00"
  9. #property strict                 //严格语法声明
  10. #property indicator_chart_window //指标显示在当前窗口声明
  11. #property indicator_buffers 0
  12. // 引入模块
  13. #include "chanlun_defines.mqh"
  14. #include "fun_chanlun_merge.mqh"
  15. #include "fun_chanlun_cache.mqh"
  16. #include "draw_chanlun_merge.mqh"
  17. //+------------------------------------------------------------------+
  18. //| Custom indicator initialization function 加载初始化函数          |
  19. //+------------------------------------------------------------------+
  20. int OnInit ()
  21.   {
  22.     Print("chan指标开始!尝试从硬盘加载历史合并缓存。");
  23.    
  24.     // 尝试从硬盘加载历史合并缓存
  25.     if(LoadMergeCache() == false)
  26.     {
  27.         Print("无有效缓存,首次计算将执行全量合并");
  28.     }
  29.     // 加载成功的话,g_mergeArray 里就已经有完整的历史合并结果了,后续只算增量
  30.     // 加载失败也没关系,第一次 OnCalculate 会自动跑全量合并
  31.    
  32.     Print("chan指标初始化成功!");
  33.    
  34.     return INIT_SUCCEEDED;
  35.   }
  36.   
  37. //+------------------------------------------------------------------+
  38. //| Custom indicator iteration function “自定义指标”迭代函数         |
  39. //+------------------------------------------------------------------+
  40. int OnCalculate(const int32_t rates_total,             // 1. 当前图表总共有多少根K线
  41.                 const int32_t prev_calculated,         // 2. 上一次已经计算完成的K线数量(增量核心),含上次结束时未收盘的那根
  42.                 const datetime &time[],                // 3. 每根K线的开盘时间数组
  43.                 const double &open[],                  // 4. 开盘价数组
  44.                 const double &high[],                  // 5. 最高价数组
  45.                 const double &low[],                   // 6. 最低价数组
  46.                 const double &close[],                 // 7. 收盘价数组
  47.                 const long &tick_volume[],             // 8. tick成交量数组
  48.                 const long &volume[],                  // 9. 标准成交量数组
  49.                 const int32_t &spread[])               // 10. 点差数组
  50.   {
  51.    Print("当前K线图表加载成功,共加载K线=", ArraySize(time),"个。");
  52.       
  53.     // ==================================================
  54.     // 分支1:首次加载 / 数据重置(prev_calculated == 0)
  55.     // 触发场景:指标刚加载、切换品种周期、历史数据修正、拖拽加载更早K线
  56.     // ==================================================
  57.     if(prev_calculated == 0)
  58.     {
  59.         // 调用统一缓存校验函数,拿到状态
  60.         CacheCheckResult status = CheckCacheAll(time, high, low);
  61.                
  62.         switch(status)
  63.         {
  64.             // --- 缓存失效:全量重算 ---
  65.             case CACHE_INVALID:
  66.                 FullMerge(high, low, time);
  67.             Print("执行全量合并,完成后组数=", ArraySize(g_mergeArray));
  68.                 break;
  69.                
  70.             // --- 缓存有效:找到断点,增量续算 ---
  71.             case CACHE_CONTINUE:
  72.             {
  73.                 // 找到缓存最后一组对应的K线下标
  74.                 datetime lastCacheTime = g_mergeArray[ArraySize(g_mergeArray)-1].time;
  75.                 int breakIdx = -1;
  76.                
  77.                 // 从后往前找,匹配更快
  78.                 for(int i = rates_total - 1; i >= 0; i--)
  79.                 {
  80.                     if(time[i] == lastCacheTime)
  81.                     {
  82.                         breakIdx = i;
  83.                         break;
  84.                     }
  85.                 }
  86.                
  87.                 // 从下一根K线开始增量合并
  88.                 if(breakIdx >= 0)
  89.                 {
  90.                    IncrementMerge(high, low, time);
  91.                
  92.                 Print("执行增量续算,完成后组数=", ArraySize(g_mergeArray));
  93.                 break;
  94.                   
  95.                 }
  96.                 else
  97.                 {
  98.                     // 兜底:找不到断点就全量重算
  99.                     FullMerge(high, low, time);
  100.                 }
  101.                 break;
  102.             }
  103.                
  104.             // 加载不完全,清空已有的旧合并方框,避免显示错位
  105.             case CACHE_UNFINISHED:
  106.              if(ShowMergeBox)
  107.                 {
  108.                     int objTotal = ObjectsTotal(0);
  109.                     for(int i = objTotal - 1; i >= 0; i--)
  110.                     {
  111.                         string objName = ObjectName(0, i);
  112.                         if(StringFind(objName, "merge_box_", 0) == 0)
  113.                         {
  114.                             ObjectDelete(0, objName);
  115.                         }
  116.                     }
  117.                 }
  118.                 // 终端日志打印提示,用户可通过日志排查
  119.                 Print("历史K线加载未完成,合并方框暂不显示,请等待数据加载完成或刷新当前图表");
  120.          
  121.                 break;
  122.                
  123.         }
  124.     }
  125.     // ==================================================
  126.     // 分支2:正常运行(prev_calculated > 0)
  127.     // 触发场景:日常行情推进,只有末尾新增K线
  128.     // ==================================================
  129.     else
  130.     {
  131.         // 只处理新增K线,性能最优
  132.         IncrementMerge(high, low, time);
  133.         
  134.         
  135.         // ===== 插入调试日志 =====
  136.         Print("【调试】正常运行增量合并,完成后组数=", ArraySize(g_mergeArray));
  137.         // ========================
  138.         
  139.         
  140.     }
  141.     // ==================================================
  142.     // 最后一步:绘制合并方框
  143.     // ==================================================
  144.     /*
  145.     // ===== 插入调试日志 =====
  146.     Print("【调试】进入绘图环节 ShowMergeBox=", ShowMergeBox);
  147.     // ========================
  148.     */
  149.    
  150.     if(ShowMergeBox)
  151.     {
  152.         int cacheSize = ArraySize(g_mergeArray);
  153.         // 双重校验:有有效数据 且 最后一组时间在当前K线范围内,才绘图
  154.         if(cacheSize >= 2)
  155.         {
  156.            
  157.           DrawMergeBoxes(time);
  158.          
  159.         /*
  160.         // ===== 插入调试日志 =====
  161.         Print("【调试】绘图函数调用完成");
  162.         // ========================
  163.         */
  164.             
  165.         }
  166.     }
  167.     // 标准返回,系统会记录为下一次的prev_calculated
  168.     return rates_total;
  169. }
  170.   
  171. void OnDeinit(const int reason)
  172. {
  173.    
  174.    
  175.     // 【最优先】先保存最新数据,再做其他清理
  176.     if(ArraySize(g_mergeArray) >= 2)
  177.     {
  178.         SaveMergeCache();
  179.        Print("数据保存函数调用成功!");
  180.     }
  181.    
  182.    
  183. }
复制代码

二、下面是全局声明defines部分的代码
  1. //+------------------------------------------------------------------+
  2. //|                   缠论指标公共数据定义文件    chanlun_defines.mqh |
  3. //|                                             Copyright 2026,ZHAO. |
  4. //|                                                      qq317008881 |
  5. //+------------------------------------------------------------------+
  6. #property copyright "Copyright 2026,ZHAO."
  7. #property link      "qq317008881"
  8. #ifndef CHANLUN_DEFINES_H
  9. #define CHANLUN_DEFINES_H
  10. // 合并K线单元缓存数据结构体
  11. struct MergedKLine     
  12. {
  13.     datetime        time;          // 该合并组最新一根原始K线的开盘时间(唯一标识,用于断点校验)
  14.     double          high;          // 合并后的高点
  15.     double          low;           // 合并后的低点
  16.     int             startIdx;      // 对应原始K线的起始索引(最老一根,可选,方便回溯)
  17.     int             endIdx;        // 对应原始K线的结束索引(最新一根,断点用)
  18.     int             trend;         // 即合并方向,新k线组第一根k线与它前一根k线判断
  19. };                                 // 合并单元结构体有六个参数
  20. //+------------------------------------------------------------------+
  21. //| 全局状态与缓存                                                   |
  22. //+------------------------------------------------------------------+
  23. MergedKLine  g_mergeArray[];  // 内存缓存:存放合并后的K线组合,是一个结构体数组
  24. //+------------------------------------------------------------------+
  25. //| 合并k线方框显示开关                                              |
  26. //+------------------------------------------------------------------+
  27. input bool ShowMergeBox = true;  // 显示合并K线方框
  28. #endif
复制代码

三、下面是合并函数merge部分的代码
  1. //+------------------------------------------------------------------+
  2. //|                            包含K线合并函数 fun_chanlun_merge.mqh |
  3. //|                                             Copyright 2026,ZHAO. |
  4. //|                                                      qq317008881 |
  5. //+------------------------------------------------------------------+
  6. #property copyright "Copyright 2026,ZHAO."
  7. #property link      "qq317008881"
  8. #ifndef FUN_CHANLUN_MERGE_H
  9. #define FUN_CHANLUN_MERGE_H
  10. #include "chanlun_defines.mqh"
  11. //+------------------------------------------------------------------+
  12. //| 函数实现                                                         |
  13. //+------------------------------------------------------------------+  
  14. //*****************************************************************************************************************************                  
  15. //壹.检查两个K线是否满足包含关系的函数,true为符合,false为不符合
  16. //highNew/lowNew,当前新K线(要比较的)高价/低价,highOld/lowOld,此前旧K线(被比较的)高价/低价,
  17. bool CheckMergeStatus(double highNew, double lowNew,
  18.                       double highOld, double lowOld)  //默认新高、新低、旧高、旧低,防止后续传参错误
  19. {
  20.     // 逻辑表达式本身可以得到布尔值,直接返回
  21.     return (highNew >= highOld && lowNew <= lowOld)   
  22.            || (highOld >= highNew && lowOld <= lowNew);  
  23. }
  24. //*****************************************************************************************************************************   
  25. //贰.检查K线合并方向,1为向上合并,-1为向下合并
  26. //highNew/lowNew,此前较新K线(要比较的)高价/低价,highOld/lowOld,此前第二根K线(更老的,被比较的)高价/低价,
  27. int CheckMergeTrend(double highNew, double lowNew,
  28.                     double highOld, double lowOld)   //默认新高、新低、旧高、旧低,防止后续传参错误
  29. {
  30.    bool isUp   = (highNew > highOld) && (lowNew > lowOld); //高高向上
  31.    bool isDown = (highNew < highOld) && (lowNew < lowOld); //低低向下
  32.    
  33.    if(isUp == true)   
  34.    {
  35.    return  1;
  36.    }
  37.    if(isDown == true)
  38.    {
  39.    return -1;
  40.    }
  41.    else
  42.    {
  43.    return 0; // 输入不合法:两根K线存在包含或交叉
  44.    }
  45. }
  46. //*****************************************************************************************************************************   
  47. //叁.全量合并函数                     
  48. void FullMerge(const double &high[],      //引入K线最高价数组
  49.                const double &low[],       //引入K线最低价数组
  50.                const datetime &time[])    //引入K线开盘时间数组   
  51. {
  52.   // 异常兜底:K线数量不足3根时无法启动合并,直接返回
  53.   // high:数组high[],ArraySize:求数组元素个数,总K线数<3个
  54.   int ratesTotal = ArraySize(high);
  55.   if(ratesTotal < 3 )
  56.    {
  57.     ArrayResize(g_mergeArray, 0); //ArrayResize:数组修改函数,把g_mergeArray数组归零
  58.     return;
  59.    }
  60.   
  61.   // --------------------------
  62.   // 1. 初始化状态,即默认先输入两个K线作为合并基础
  63.   // --------------------------
  64.   ArrayResize(g_mergeArray, 0); // 把g_mergeArray数组归零,清空历史结果
  65.   // 第一个K线组(第一根K线):索引0,直接存入结果(作为方向基准的左锚点)
  66.   MergedKLine Group0;          // 声明一个结构体变量Group0,即第一组K线组,沿用MQL5编号习惯,从0开始编号
  67.     Group0.time     = time[0]; // 下标0就是第一根K线,Group0开盘时间即是0号K线开盘时间
  68.     Group0.high     = high[0]; // Group0最高价就是0号K线最高价
  69.     Group0.low      = low[0];  // Group0最低价就是0号K线最低价
  70.     Group0.startIdx = 0;       // Group0中只有一个k线,起止标号都是0号K线标号
  71.     Group0.endIdx   = 0;       // Group0中只有一个k线,起止标号都是0号K线标号
  72.     Group0.trend    = 0;       // Group0是第一组k线,没有合并方向
  73.    
  74.   ArrayResize(g_mergeArray, 1); //把数组g_mergeArray长度变成1,即增加一个元素
  75.   g_mergeArray[0] = Group0;     //把0号k线的数据赋值给数组g_mergeArray中下标0的元素
  76.   //第二个K线组:索引1,与索引0一样,直接存入结果
  77.   MergedKLine Group1;
  78.     Group1.time     = time[1];  // 下标1就是第二根K线
  79.     Group1.high     = high[1];
  80.     Group1.low      = low[1];
  81.     Group1.startIdx = 1;
  82.     Group1.endIdx   = 1;
  83.   //先默认前两根K线不合并,作为后续可能合并的方向条件
  84.   //先判断第一组合并方向
  85.   int Trend = CheckMergeTrend(high[1], low[1], high[0], low[0]);   //保持先新后旧的排序,
  86.     //只在这里做一次兜底:初始方向不明时,默认按向上处理
  87.   if(Trend == 0) Trend = 1;
  88.     Group1.trend    = Trend;    // Group1是第二组k线,可以与此一组k线判断方向了
  89.       
  90.   ArrayResize(g_mergeArray, 2); //把数组g_mergeArray长度变成2
  91.   g_mergeArray[1] = Group1;   //把1号k线的数据赋值给数组g_mergeArray中下标1的元素
  92.    
  93.   //合并主循环,从第三根K线即下标2的K线开始,一直到最新完成的k线(即总K线数减2)结束
  94.   for(int i = 2; i <= (ratesTotal-2); i++)
  95.    {
  96.     int GroupLastIdx      = ArraySize(g_mergeArray) - 1;         //得到数组g_mergeArray里最后一个(第Last个)元素的下标
  97.     double GroupHighLast  = g_mergeArray[GroupLastIdx].high;     //得到数组g_mergeArray里最后一个(第Last个)元素的高价
  98.     double GroupLowLast   = g_mergeArray[GroupLastIdx].low;      //得到数组g_mergeArray里最后一个(第Last个)元素的低价
  99.     int GroupTrendLast    = g_mergeArray[GroupLastIdx].trend;    // 直接从最后一组取当前合并方向
  100.     //如果符合合并关系,K2(Ki)会合并到K1(GroupLast)所在的K线组中去,需要修改当前k线组的最新K线开盘时间、最高价、最低价、结束k线下标
  101.     if(CheckMergeStatus(high[i], low[i], GroupHighLast, GroupLowLast) == true)        //调用是否符合合并关系函数进行判断                     
  102.      {
  103.       //情况1:向上合并
  104.       if(GroupTrendLast == 1)
  105.        {
  106.         g_mergeArray[GroupLastIdx].high = MathMax(GroupHighLast, high[i]); //高价取两个K线高价中的较高者
  107.         g_mergeArray[GroupLastIdx].low  = MathMax(GroupLowLast, low[i]);   //低价取两个K线低价中的较高者
  108.        }
  109.       //情况2:向下合并
  110.       else
  111.        {
  112.         g_mergeArray[GroupLastIdx].high = MathMin(GroupHighLast, high[i]); //高价取两个K线高价中的较低者
  113.         g_mergeArray[GroupLastIdx].low  = MathMin(GroupLowLast, low[i]);  //低价取两个K线低价中的较低者
  114.        }
  115.    
  116.      g_mergeArray[GroupLastIdx].endIdx  = i;       // 更新结束K线索引,开始K线索引不需要更新
  117.      g_mergeArray[GroupLastIdx].time    = time[i]; // 更新组时间为最新K线时间
  118.   
  119.      }
  120.     else  // --- 不包含:当前K线做成新组合,追加到数组末尾 ---
  121.      {
  122.       MergedKLine GroupNew;                  // 声明一个结构体变量GroupNew,即新K线组
  123.         GroupNew.time     = time[i];         // 把新k线i赋值给结构体变量GroupNew
  124.         GroupNew.high     = high[i];
  125.         GroupNew.low      = low[i];
  126.         GroupNew.startIdx = i;
  127.         GroupNew.endIdx   = i;
  128.       //确认新K线与旧k线组不能合并,二者判断后续可能合并的方向条件
  129.         int newTrend = CheckMergeTrend(high[i], low[i], GroupHighLast, GroupLowLast);   //保持先新后旧的排序,
  130.         GroupNew.trend    = newTrend;           // GroupNew.是新k线组,获得后续合并方向  
  131.       
  132.       int len = ArraySize(g_mergeArray);     //在循环体内,每次循环的数组数量不固定,要用变量,后续每次循环到此都会加1
  133.       ArrayResize(g_mergeArray, len + 1);    //给g_mergeArray扩容,加一个元素
  134.       g_mergeArray[len] = GroupNew;          //把新K线组GroupNew的数据赋值给g_mergeArray      
  135.   
  136.      }
  137.    }
  138. }   
  139. //*****************************************************************************************************************************   
  140. //肆.增量合并函数                                                     
  141. void IncrementMerge(const double &high[],   //输入参数与全量合并函数一样,
  142.                     const double &low[],
  143.                     const datetime &time[])
  144. {
  145. // --------------------------
  146. // 1. 基础数值计算
  147. // --------------------------
  148. int ratesTotal  = ArraySize(high);     // 内部自行获取当前加载总K线数量
  149. int endIdx      = ratesTotal - 2;      // 最新闭合K线索引
  150. int arraySize   = ArraySize(g_mergeArray);
  151. // --------------------------
  152. // 2. 边界兜底检查
  153. // --------------------------
  154. // 兜底1:合并数组不足2组,说明未初始化,自动执行全量合并
  155. if(arraySize < 2)
  156. {
  157.    FullMerge(high, low, time);
  158.    return;
  159. }
  160. // 兜底2:K线数量不足3根,无合并意义,直接退出
  161. if(ratesTotal < 3) return;
  162. // --------------------------
  163. // 3. 计算增量遍历起点
  164. // --------------------------
  165. int GroupLastIdx = arraySize - 1;                              //得到数组g_mergeArray里最后一个(第Last个)元素的下标
  166. int EndLastIdx   = g_mergeArray[GroupLastIdx].endIdx;          // 最后一个元素的最后一根原始K线下标
  167. int GroupNewIdx  = EndLastIdx + 1;                             // 本次从下一根开始处理
  168. // 没有新的闭合K线,无需处理
  169. //GroupNewIdx需要处理的k线下标,ratesTotal-2 最新完成的k线下标
  170. //如果需要处理的k线比最新完成的K线还新,那它就是最新未完成的k线,不处理
  171. if(GroupNewIdx > (ratesTotal-2))  return;                             
  172.   
  173. // --------------------------
  174. // 4. 增量主循环(与全量循环逻辑完全一致)
  175. // --------------------------
  176. for(int i = GroupNewIdx; i <= (ratesTotal-2); i++) //循环开始k线是K线组最后一根k线的下一根k线,循环结束k线是最新一根完成k线
  177. {
  178.         // 取出当前最后一个合并组的高低点及合并方向作为基准
  179.         int GroupLastIdx = ArraySize(g_mergeArray) - 1;         //得到数组g_mergeArray里最后一个(第Last个)元素的下标
  180.         double GroupHighLast  = g_mergeArray[GroupLastIdx].high;     //得到数组g_mergeArray里最后一个(第Last个)元素的高价
  181.         double GroupLowLast   = g_mergeArray[GroupLastIdx].low;      //得到数组g_mergeArray里最后一个(第Last个)元素的低价
  182.         int GroupTrendLast    = g_mergeArray[GroupLastIdx].trend;    // 直接从最后一组取当前合并方向
  183.         if(CheckMergeStatus(high[i], low[i], GroupHighLast, GroupLowLast) == true)  // 调用判断包含关系函数
  184.         {
  185.             // 包含:更新最后一个合并组的高低点与结束位置
  186.             if(GroupTrendLast == 1) // 向上合并:取更高的高点、更高的低点
  187.             {
  188.                 g_mergeArray[GroupLastIdx].high = MathMax(GroupHighLast, high[i]);
  189.                 g_mergeArray[GroupLastIdx].low  = MathMax(GroupLowLast, low[i]);
  190.             }
  191.             else // 向下合并:取更低的高点、更低的低点
  192.             {
  193.                 g_mergeArray[GroupLastIdx].high = MathMin(GroupHighLast, high[i]);
  194.                 g_mergeArray[GroupLastIdx].low  = MathMin(GroupLowLast, low[i]);
  195.             }
  196.             g_mergeArray[GroupLastIdx].endIdx = i;
  197.             g_mergeArray[GroupLastIdx].time   = time[i];
  198.         }
  199.         else
  200.         {
  201.             // 不包含:追加新的独立合并组
  202.             MergedKLine GroupNew;                  // 声明一个结构体变量GroupNew,即新K线组
  203.               GroupNew.time     = time[i];         // 把新k线i赋值给结构体变量GroupNew
  204.               GroupNew.high     = high[i];
  205.               GroupNew.low      = low[i];
  206.               GroupNew.startIdx = i;
  207.               GroupNew.endIdx   = i;
  208.             //确认新K线与旧k线组不能合并,二者判断后续可能合并的方向条件
  209.           int newTrend     = CheckMergeTrend(high[i], low[i], GroupHighLast, GroupLowLast);   //保持先新后旧的排序,
  210.           GroupNew.trend   = newTrend;           // GroupNew.是新k线组,获得后续合并方向  
  211.          
  212.           int len = ArraySize(g_mergeArray);     //g_mergeArray第一次循环到此已有2个元素,后续每次循环到此都会加1
  213.           ArrayResize(g_mergeArray, len + 1);    //给g_mergeArray扩容,加一个元素
  214.           g_mergeArray[len] = GroupNew;          //把新K线组GroupNew的数据赋值给g_mergeArray      
  215.   
  216.         }
  217.     }
  218. }                    
  219.                            
  220. #endif
  221. /*
  222. 缠论中对"包含K线"的定义:
  223. 1.两根相邻 K 线中,如果一根 K 线的最高价 ≥ 另一根的最高价,且最低价 ≤ 另一根的最低价,
  224.   也就是一根 K 线的高低点完全包裹住另一根,就称这两根 K 线存在「包含关系」。
  225.   
  226. 2.缠论中对包含处理顺序的规定:从左到右,依次合并。必须按照 K 线出现的时间顺序,从最老
  227.   到最新(从左到右)依次处理,先合并前面的,再用合并后的新 K 线和后面的 K 线比较,不能
  228.   跳着处理、不能从右往左处理。
  229.   
  230. 3.缠论中对包含处理合并方向的规定:不是看包含的两根 K 线谁高谁低,而是由「这组包含关系之
  231.   前,最近一根没有包含关系的 K 线」来决定:取包含组左侧最近的一根独立 K 线,和包含组的第
  232.   一根 K 线比较:
  233.   (1)如果独立 K 线高点 < 包含组第一根的高点,属于上升趋势中的包含,按向上合并处理;
  234.   (2)如果独立 K 线高点 > 包含组第一根的高点,属于下降趋势中的包含,按向下合并处理;
  235.   (3)非包含关系的两根k线,如果一个高点高于另一个的,那么低点也一定高于另一个的,反之同
  236.    理,代码中同时比较傲低点,也是为了增加容错率。
  237.    
  238. 4.缠论中对合并取值的规定:
  239.   (1)向上合并(高高原则):合并后的新 K 线,高点取两根 K 线高点的最大值,低点取两根 K 线
  240.   低点的最大值。也就是:`新高 = max(高1, 高2)`,`新低 = max(低1, 低2)`;
  241.   (2)向下合并(低低原则):合并后的新 K 线,高点取两根 K 线高点的最小值,低点取两根 K 线
  242.   低点的最小值。
  243.   
  244. 5.缠论中对于连续包含的处理规定:如果出现三根及以上连续的包含 K 线,就从左到右滚动合并:
  245.   (1)先合并前两根,得到一个新的 K 线组;
  246.   (2)用这根新 K 线和第三根比较,如果还是包含关系,继续按同一个方向合并;
  247.   (3)直到遇到不包含的 K 线,输出最终合并后的 K 线,继续往后遍历。
  248.   (4)连续包含的方向,由第一组包含的前序方向决定,中间不会变方向。
  249.   
  250. 6.关于起始边界的处理:图表最开头的前两根 K 线怎么弄,禅师没说。我们可以给出最简单的处理方法,
  251.    把这两个K线当作独立k线组,由它们推导出第三根可能合并的方向,如果二者存在合并关系,那就默
  252.    认第三根k线要合并的话就按多头方向合并。有人说缠论是从头推理,开头错了就都错了,实际上只要
  253.    出现连续的三根不包含的k线,那它们前面合并方向是向上还是向下都没意义了,我管这个叫方向阻断。
  254.    实际操作中往往要算几千几万根k线,存在大量的单边行情,也就是方向阻断效应,头几根丢掉也无所
  255.    谓,没什么影响。
  256. 7.补充规定:缠论中并没有特殊要求,呈现包含关系的两个K线是左边的长一点,还是右边的长一点,所以
  257.   我们默认无论是左K包右K,还是右K包左K,都是包含关系,都按上面的要求处理。
  258.   
  259. 8.显示要求:这一分析步骤,在代码中最终只形成了一个处理后的K线数据组,我们是看不到的。为了让我
  260.   们可以看到代码分析的成果,评判它是否正确,需要进行可视化处理。简单来说,把向上合并的K线组用
  261.   蓝色方框框起来,向下合并的K线组用红色方框框起来。
  262.   
  263. 9.记录要求:程序中生成的k线数组是放在内存缓存中的,关闭程序后就被清理,下次打开又重新计算一遍,
  264.   为了避免重复计算浪费性能,同时也是给程序存档,我们需要设计硬盘缓存功能,数据以二进制存入硬盘,
  265.   同时还需要另设计一个脚本一建转化该文档,方便用户阅读。
  266.   
  267.   */
复制代码

四、下面是绘图函数draw merge部分的代码
  1. //+------------------------------------------------------------------+
  2. //|                                           draw_chanlun_merge.mqh |
  3. //|                                             Copyright 2026,ZHAO. |
  4. //|                                                      qq317008881 |
  5. //+------------------------------------------------------------------+
  6. #property copyright "Copyright 2026,ZHAO."
  7. #property link      "qq317008881"
  8. #ifndef DRAW_CHANLUN_MERGE_H
  9. #define DRAW_CHANLUN_MERGE_H
  10. #include "chanlun_defines.mqh"
  11. //+------------------------------------------------------------------+
  12. //| 在主图绘制合并K线组的方框                                        |
  13. //| 参数:time[] K线时间数组                                         |
  14. //+------------------------------------------------------------------+
  15. void DrawMergeBoxes(const datetime &time[]) //DrawMergeBoxes:绘制合并方框,自定义函数
  16. {
  17.     // ========== 1. 先清空所有旧的合并方框,避免残留 ==========
  18.     // 倒序遍历主图所有对象,删除我们生成的方框(避免正序删除导致的索引错位)
  19.     //`ObjectsTotal` 内置图表图形对象统计函数,0代表默认当前图表,返回一个整数,代表图表中符合条件的图形对象总个数。
  20.     // 识别我们自己画的对象:以 merge_box_ 开头
  21.     int delCount = 0;
  22.     int total = ObjectsTotal(0);
  23.     for(int i = total-1; i >= 0; i--)
  24.     {
  25.         string name = ObjectName(0, i);
  26.         if(StringFind(name, "merge_box_", 0) == 0)
  27.         {
  28.             ObjectDelete(0, name);//`ObjectDelete` 官方内置图形对象删除函数
  29.             delCount++;
  30.         }
  31.     }
  32.    
  33.     Print("【调试】清空旧方框数量=", delCount);
  34.    
  35.     int arraySize   = ArraySize(g_mergeArray);
  36.     //兜底设置,如果g_mergeArray里面一个成员都没有,那就直接返回
  37.     if(arraySize < 1) return;
  38.    
  39.     int ratesTotalT  = ArraySize(time);
  40.     // ========== 2. 遍历每个合并组,逐个绘制方框 ==========
  41.     for(int i = 0; i < arraySize; i++)
  42.     {
  43.         // 边界兜底:索引非法则跳过
  44.         if(g_mergeArray[i].startIdx < 0 ||
  45.            g_mergeArray[i].endIdx   >= ratesTotalT ||
  46.            g_mergeArray[i].startIdx >  g_mergeArray[i].endIdx)
  47.             continue;
  48.       
  49.         //`IntegerToString` 方内置的整型转字符串函数,把数组中的序号i转化为对应数字,相当于给方框按照数据成员序列编号
  50.         string boxName = "merge_box_" + IntegerToString(i);
  51.         // 计算两个对角坐标,即起止k线的时间和价格参数
  52.         datetime leftTime  = time[g_mergeArray[i].startIdx];
  53.         datetime rightTime = time[g_mergeArray[i].endIdx];
  54.         double topPrice    = g_mergeArray[i].high;
  55.         double bottomPrice = g_mergeArray[i].low;
  56.         
  57.         // 直接传入两个对角的时间+价格,一步定位
  58.         //`ObjectCreate` 图形对象创建函数,是所有自定义绘图的核心入口,矩形、趋势线、文字、箭头等都靠它创建。
  59.         //返回值是布尔值,代表是否创建成功,如果创建不成功,跳过,进行下一个
  60.         if(!ObjectCreate(0, boxName, OBJ_RECTANGLE, 0, leftTime, topPrice, rightTime, bottomPrice))
  61.         {
  62.             continue;
  63.         }
  64.         // --- 设置颜色和样式 ---
  65.         color borderColor;
  66.         if(g_mergeArray[i].trend == 1)         borderColor = clrBlue;   // 向上合并 → 蓝色
  67.         else if(g_mergeArray[i].trend == -1)   borderColor = clrRed;    // 向下合并 → 红色
  68.         else                                   borderColor = clrGray;   // 初始锚点组 → 灰色
  69.        //`ObjectSetInteger` 官方图表对象体系中的整数型属性设置函数,专门用于给已创建的图形对象设置整数类型的属性(颜色、线宽、线条样式、布尔开关等)。
  70.        //其第三个参数属性枚举值,指定要修改对象的哪一项属性,是最核心的参数。
  71.         ObjectSetInteger(0, boxName, OBJPROP_COLOR, borderColor);    // 边框颜色
  72.         ObjectSetInteger(0, boxName, OBJPROP_STYLE, STYLE_SOLID);    // 实线边框
  73.         ObjectSetInteger(0, boxName, OBJPROP_WIDTH, 1);              // 边框宽度1
  74.         ObjectSetInteger(0, boxName, OBJPROP_FILL, true);           // 填充(透明)
  75.         ObjectSetInteger(0, boxName, OBJPROP_BACK, false);           // 显示在K线前方
  76.         ObjectSetInteger(0, boxName, OBJPROP_SELECTABLE, false);     // 禁止鼠标选中,避免误操作
  77.     }
  78.    
  79.   
  80.         
  81. }
  82.                         
  83. #endif
复制代码

五、下面是缓存部分cache部分的代码
  1. //+------------------------------------------------------------------+
  2. //|                            包含K线合并函数 fun_chanlun_cache.mqh |
  3. //|                                             Copyright 2026,ZHAO. |
  4. //|                                                      qq317008881 |
  5. //+------------------------------------------------------------------+
  6. #property copyright "Copyright 2026,ZHAO."
  7. #property link      "qq317008881"
  8. #ifndef FUN_CHANLUN_CACHE_H
  9. #define FUN_CHANLUN_CACHE_H
  10. #include "chanlun_defines.mqh"
  11. //+------------------------------------------------------------------+
  12. //| 函数实现                                                         |
  13. //+------------------------------------------------------------------+  
  14. //*****************************************************************************************************************************                  
  15. //+------------------------------------------------------------------+
  16. //| 壹、时间周期显示转换函数                                         |
  17. //| 把Period()函数默认返回的数值转化为具体文字,方便阅读             |
  18. //+------------------------------------------------------------------+
  19. string PeriodToString(ENUM_TIMEFRAMES period)
  20. {
  21.     switch(period)
  22.     {
  23.         case PERIOD_M1:  return "M1";
  24.         case PERIOD_M5:  return "M5";
  25.         case PERIOD_M15: return "M15";
  26.         case PERIOD_M30: return "M30";
  27.         case PERIOD_H1:  return "H1";
  28.         case PERIOD_H4:  return "H4";
  29.         case PERIOD_D1:  return "D1";
  30.         case PERIOD_W1:  return "W1";
  31.         case PERIOD_MN1: return "MN1";
  32.         default:         return IntegerToString((int)period); // 未知周期兜底用数字
  33.     }
  34. }
  35. //*****************************************************************************************************************************                  
  36. //+------------------------------------------------------------------+
  37. //| 贰、保存合并缓存到硬盘文件                                       |
  38. //| 返回值:true=保存成功,false=保存失败                            |
  39. //+------------------------------------------------------------------+
  40. #define MERGE_CACHE_MAGIC   0x434C4D47 //缓存身份标记
  41. #define MERGE_CACHE_VERSION 1          //缓存版本标记
  42. bool SaveMergeCache()
  43. {
  44.     // 文件名自动拼接品种+周期,不同品种/周期的缓存互不干扰
  45.     //StringFormat:内置系统函数,按照指定的格式模板,把多种不同类型的数据(整数、小数、字符串、时间等)拼接成一个规整的字符串。
  46.     string fileName = StringFormat("merge_cache_%s_%s.bin", Symbol(), PeriodToString(Period()));
  47.    
  48.     // 以二进制写入模式打开文件,覆盖旧文件,`FileOpen` 是 MQL5 内置的**核心文件操作入口函数**,返回一个正整数,代表文件句柄
  49.     int fileHandle = FileOpen(fileName, FILE_BIN | FILE_WRITE );
  50.     if(fileHandle == INVALID_HANDLE)
  51.     {
  52.         Print("保存合并缓存失败,错误码:", GetLastError());
  53.         return false;
  54.     }
  55.    
  56.     // 缓存文件身份标识:魔数 + 版本
  57.     FileWriteInteger(fileHandle, MERGE_CACHE_MAGIC);
  58.     FileWriteInteger(fileHandle, MERGE_CACHE_VERSION);   
  59.     // 第1步:先写入数组总长度(读取时先知道要开多大空间)
  60.     int arraySize   = ArraySize(g_mergeArray);
  61.     FileWriteInteger(fileHandle, arraySize);
  62.    
  63.     // 第2步:逐个写入每个合并组的字段,顺序必须和结构体完全对应
  64.     for(int i = 0; i < arraySize; i++)
  65.     {
  66.         FileWriteLong   (fileHandle, g_mergeArray[i].time);     // datetime 本质是长整型
  67.         FileWriteDouble (fileHandle, g_mergeArray[i].high);
  68.         FileWriteDouble (fileHandle, g_mergeArray[i].low);
  69.         FileWriteInteger(fileHandle, g_mergeArray[i].startIdx);
  70.         FileWriteInteger(fileHandle, g_mergeArray[i].endIdx);
  71.         FileWriteInteger(fileHandle, g_mergeArray[i].trend);      // 同步保存方向字段
  72.     }
  73.     FileFlush(fileHandle);
  74.     FileClose(fileHandle);
  75.     return true;
  76. }
  77. //*****************************************************************************************************************************                        
  78. //+------------------------------------------------------------------+
  79. //| 叁、从硬盘加载二进制合并缓存到内存数组                           |
  80. //| 返回值:true=加载成功,false=无缓存/缓存损坏/读取失败            |
  81. //+------------------------------------------------------------------+
  82. bool LoadMergeCache()
  83. {
  84.     // 生成与保存规则完全一致的文件名(品种+可读周期)
  85.     string fileName  = StringFormat("merge_cache_%s_%s.bin", Symbol(), PeriodToString(Period()));
  86.    
  87.     // 以二进制只读模式打开文件
  88.     int fileHandle = FileOpen(fileName, FILE_BIN | FILE_READ);
  89.     if(fileHandle == INVALID_HANDLE)
  90.     {
  91.         // 文件不存在属于正常情况(首次运行),不算错误
  92.         return false;
  93.     }
  94.     // 头部校验
  95.     int magic   = FileReadInteger(fileHandle);
  96.     int version = FileReadInteger(fileHandle);
  97.     if(magic != MERGE_CACHE_MAGIC || version != MERGE_CACHE_VERSION)
  98.     {
  99.         FileClose(fileHandle);
  100.         return false;
  101.     }
  102.    
  103.     // 数据合法性校验:不足2组无意义,直接丢弃缓存
  104.     // FileReadInteger:读取函数,返回读取到的元素个数
  105.     int fileCount = FileReadInteger(fileHandle);
  106.     if(fileCount < 2)
  107.     {
  108.         FileClose(fileHandle);
  109.         return false;
  110.     }
  111.     // 调整全局数组合适大小,准备接收数据,如果加载不成功返回失败
  112.     if(ArrayResize(g_mergeArray, fileCount) != fileCount)
  113.     {
  114.         ArrayResize(g_mergeArray, 0);
  115.         FileClose(fileHandle);
  116.         return false;
  117.     }
  118.     // 读取顺序必须和保存函数的写入顺序完全一致
  119.     for(int i = 0; i < fileCount; i++)
  120.     {
  121.         g_mergeArray[i].time     = (datetime)FileReadLong(fileHandle);     // 时间(长整型转datetime)
  122.         g_mergeArray[i].high     = FileReadDouble(fileHandle);            // 高点
  123.         g_mergeArray[i].low      = FileReadDouble(fileHandle);            // 低点
  124.         g_mergeArray[i].startIdx = FileReadInteger(fileHandle);           // 起始K线索引
  125.         g_mergeArray[i].endIdx   = FileReadInteger(fileHandle);           // 结束K线索引
  126.         g_mergeArray[i].trend    = FileReadInteger(fileHandle);           // 相对前一组的合并方向
  127.    
  128.     // 字段合法性校验
  129.         if(g_mergeArray[i].high < g_mergeArray[i].low ||
  130.            g_mergeArray[i].startIdx < 0 ||
  131.            g_mergeArray[i].startIdx > g_mergeArray[i].endIdx ||
  132.            (g_mergeArray[i].trend != -1 &&
  133.             g_mergeArray[i].trend !=  0 &&
  134.             g_mergeArray[i].trend !=  1))
  135.         {
  136.             Print("缓存字段损坏,i=", i);
  137.             ArrayResize(g_mergeArray, 0);
  138.             FileClose(fileHandle);
  139.             return false;
  140.         }
  141.     }
  142.     // 完整性校验
  143.     if(FileTell(fileHandle) != FileSize(fileHandle))
  144.     {
  145.         Print("缓存文件长度不匹配,可能被截断");
  146.         ArrayResize(g_mergeArray, 0);
  147.         FileClose(fileHandle);
  148.         return false;
  149.     }
  150.    
  151.     // 关闭文件,释放系统资源
  152.     FileClose(fileHandle);
  153.    
  154.     Print("历史合并缓存加载成功,共", fileCount, "个合并组");
  155.     return true;
  156. }   
  157. //*****************************************************************************************************************************      
  158. //| 缓存校验结果状态,这是一个枚举  
  159. enum CacheCheckResult
  160. {
  161.     CACHE_INVALID,     // INVALID:无效的。缓存失效,需要全量重算(对应:损坏/扩充/截断/报价修正)
  162.     CACHE_CONTINUE,    // CONTINUE:继续。缓存有效,可正常增量续算(对应:正常顺延)
  163.     CACHE_UNFINISHED   // UNFINISHED:未完成的。K线加载不全,暂用现有缓存(对应:数据加载不全)
  164. };
  165. //+--------------------------------------------------------------------------------------------------------+
  166. //| 统一缓存校验函数:整合正常顺延、当前数据家在不全、历史数据扩充、历史报价静默修正、缓存文件损坏等5项检查
  167. //| CacheCheckResult:自定义返回状态枚举,返回值:缓存状态枚举
  168. //+--------------------------------------------------------------------------------------------------------+
  169. CacheCheckResult CheckCacheAll(const datetime &time[],
  170.                                const double &high[],
  171.                                const double &low[])
  172. {
  173.     Print("开始缓存校验!");
  174.    
  175.     int arraySize = ArraySize(g_mergeArray);// 获取缓存数组的长度
  176.     int ratesTotalT  = ArraySize(time);
  177.     int ratesTotalH  = ArraySize(high);
  178.     int ratesTotalL  = ArraySize(low);
  179.             
  180.      // 0. 基础数组兜底(防止越界)
  181.      if(ratesTotalT < 2 || ratesTotalH < 2 || ratesTotalL < 2)
  182.      {
  183.        Print("当前加载K线数量小于2个,请检查MT5网络状态并刷新图表界面!");
  184.        return CACHE_UNFINISHED; //如果K线数据少于2个,未加载完成
  185.      }
  186.         
  187.         
  188.     if(arraySize < 2)
  189.     {
  190.       Print("缓存数据少于2个,缓存无效!");
  191.       return CACHE_INVALID; //如果缓存数据少于2个,缓存无效
  192.     }
  193.      
  194.      
  195.      // 1. 缓存损坏校验
  196.     if(g_mergeArray[0].high <= g_mergeArray[0].low)
  197.     {
  198.       Print("缓存损坏!");
  199.       return CACHE_INVALID; //如果0号缓存数据高点低于或等于低点,缓存已损坏,无效
  200.     }
  201.       
  202.     if(g_mergeArray[arraySize-1].high <=
  203.        g_mergeArray[arraySize-1].low)
  204.     {
  205.       Print("缓存损坏!");
  206.       return CACHE_INVALID; //如果最大下标号缓存数据高点低于或等于低点,缓存已损坏,无效
  207.     }
  208.       
  209.     if(g_mergeArray[0].time >=
  210.        g_mergeArray[arraySize-1].time)
  211.     {
  212.       Print("缓存损坏!");
  213.       return CACHE_INVALID; //如果0号缓存数据时间晚于(即大于或等于)最大下标号,缓存已损坏,无效
  214.     }
  215.    
  216.     if(g_mergeArray[arraySize-1].startIdx < 0)
  217.     {
  218.       Print("缓存损坏!");                    
  219.       return CACHE_INVALID; //如果最后一组缓存数据起始下标小于零,缓存已损坏,无效
  220.     }
  221.    
  222.      if(g_mergeArray[arraySize-1].startIdx >
  223.         g_mergeArray[arraySize-1].endIdx)
  224.     {
  225.       Print("缓存损坏!");
  226.       return CACHE_INVALID;  //如果最后一组缓存数据起始下标大于结束下标,缓存已损坏,无效
  227.     }  
  228.     // 2. 历史扩充/截断校验
  229.     // 第一组永远是单K,endTime就是缓存最早时间
  230.     if(time[0] <
  231.       (g_mergeArray[0].time))
  232.     {
  233.       Print("加载了更早的k线,重新计算!");
  234.       return CACHE_INVALID; // 如果本次加载0号K线时间早于(即小于)缓存数据0号时间,加载了更早历史,要重新计算缓存
  235.     }
  236.      
  237.     if(time[0] >
  238.       (g_mergeArray[0].time))
  239.     {
  240.      Print("加载历史k线被截断,重新计算!");
  241.      return CACHE_INVALID; // 如果本次加载0号K线时间晚于(即大于)缓存数据0号时间,老K线被截断,要重新计算缓存
  242.     }
  243.     // 3. 加载不全校验
  244.    if((time[ratesTotalT - 1]) <
  245.      (g_mergeArray[arraySize-1].time))
  246.     {
  247.      Print("加载k线不全,请检查MT5网络状态并刷新图表界面!");
  248.      return CACHE_UNFINISHED;// 如果本次加载最大号k线时间早于(即小于)缓存数据最大号时间,加载不完全,
  249.     }
  250.     // 4. 报价静默修正校验,即检查当前加载k线价格与缓存中的价格是否一致
  251.     int lastStart = g_mergeArray[arraySize-1].startIdx; //获得最后一组的起始k线下标
  252.     int lastEnd   = g_mergeArray[arraySize-1].endIdx;   //获得最后一组的结束k线下标
  253.    
  254.     if(lastEnd >= ratesTotalT)
  255.     {
  256.      Print("历史K线可能被修正,重新计算!");
  257.     return CACHE_INVALID; //如果最后一组的结束k线下标大于或等于总k线数,说明旧k线被删除过,缓存作废
  258.     }
  259.    
  260.     if(g_mergeArray[arraySize-1].time != time[lastEnd])
  261.     {
  262.      Print("历史K线可能被修正,重新计算!");
  263.     return CACHE_INVALID;//如果最后一组的结束k线时间不等于相应k线时间按,说明旧k线调整过,缓存作废
  264.     }
  265.     // 重算当前K线中,这段区间的真实高低点
  266.     double curHigh = high[g_mergeArray[arraySize - 1].startIdx]; //定义一个变量curHigh等于缓存中最后一个数组的起始k线的最高价
  267.     double curLow  = low[g_mergeArray[arraySize - 1].startIdx];  //定义一个变量curLow等于缓存中最后一个数组的起始k线的最低价
  268.     for(int i = lastStart + 1; i <= lastEnd; i++) //从后一个k线开始到缓存数组结束k线为止
  269.     {
  270.         if(high[i] > curHigh) curHigh = high[i];//如果后面的K线价更高,就更新curHigh
  271.         if(low[i] < curLow)   curLow  = low[i]; //如果后面的K线价更低,就更新curLow
  272.     }
  273.    
  274.     // 和缓存记录的组合高低点对比,超出精度就判定失效
  275.     if(fabs(curHigh - g_mergeArray[arraySize - 1].high) >
  276.       SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE))  //SymbolInfoDouble系统自带求最小点数的函数
  277.         {
  278.         Print("历史K线可能被修正,重新计算!");
  279.         return CACHE_INVALID;
  280.         }
  281.     if(fabs(curLow - g_mergeArray[(arraySize - 1)].low) >
  282.       SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE))
  283.         {
  284.         Print("历史K线可能被修正,重新计算!");
  285.         return CACHE_INVALID;
  286.         }
  287.     // 全部通过
  288.     Print("缓存校验通过!");
  289.     return CACHE_CONTINUE;
  290. }
  291. //*****************************************************************************************************************************                        
  292. //+------------------------------------------------------------------+
  293. //|伍、清空内存中的合并数组,释放内存                                |
  294. //+------------------------------------------------------------------+
  295. void ClearMergeArray()
  296. {
  297.     ArrayResize(g_mergeArray, 0); // 数组大小设为0,内存直接释放
  298. }   
  299.             
  300. #endif
复制代码


目前完成的功能只是按缠论第一步,将有合并关系的k线合并起来,供后续顶底分型使用。绘图只是为了形象显示合并结果,以检查程序合并的是否正确。具体效果如下图所示:
微信图片_2026-10-26_110621_089.png ScreenShot_2026-10-26_110728_035.png
缠论是从前往后合并,并且有合并方向,通过红蓝色框很容易分析程序合并的对不对、有没有错漏,目前来看,我觉得还是很靠谱的。
举报

评论 使用道具

精彩评论6

peterzhu2004
DDD
| 发表于 前天 20:58 | 显示全部楼层
多谢分享代码
举报

点赞 评论 使用道具

csby78
DD
| 发表于 前天 21:31 | 显示全部楼层
看不懂哦,直接发指标出来多好
举报

点赞 评论 使用道具

ljl1212004
DD
| 发表于 前天 23:34 | 显示全部楼层
看不懂,感觉很厉害
举报

点赞 评论 使用道具

xuwl8888
D
| 发表于 昨天 12:36 | 显示全部楼层
后来我干脆用工程方法重新来过,把代码拆成全局声明
举报

点赞 评论 使用道具

仙作柒爷
D
| 发表于 昨天 17:36 | 显示全部楼层
喜欢,但是离缠论的东西太远
举报

点赞 评论 使用道具

麻爪
D
 楼主 | 发表于 1 小时前 | 显示全部楼层
仙作柒爷 发表于 2026-10-27 17:36
喜欢,但是离缠论的东西太远

这是原版缠论哦,除了禅师没有明确的一些微末细节我做了自我加工之外,完全遵照缠论的规定。只是空闲不多,等我把分型 笔 线段 中枢等等都做出来,你就能看出是缠论了。
举报

点赞 评论 使用道具

发新帖
EA交易
您需要登录后才可以评论 登录 | 立即注册

麻爪

麻爪 D

如果你想让一个人下地狱,介绍他做交易。
如果你想让一个人在地狱里永不超生,介绍他做EA交易。

Fast跟单EA