监视多币种的交易信号(第三部分):引入搜索算法·综合运用
🔍

监视多币种的交易信号(第三部分):引入搜索算法·综合运用

(3/3)·手工翻品种、漏信号的老办法在 7 个步骤里被一套可保存模板加检索逻辑彻底替掉

含代码示例 第 3/3 篇
盯十几个货币对时,多数人还在手动切图表找信号,错过交叉盘突破是常事。把品种集存成模板再让程序自己搜,比人眼扫屏省得不只一点。这篇把前面两篇的架子填上算法血肉。

「信号面板的图标与坐标映射实现」

在 MT5 的 EA 面板开发里,给按钮挂箭头图标和提示文字,本质是往 CButton 派生类写资源路径。下面这段把上行/下行箭头 bmp 放进数组,按 number 参数决定用哪个,负数则清空图标并改为居中文字。 SetIcon 里用三元表达式选路径:number>=0 取 image[number],否则置空串。随后 IsCenterText 切换文字居中状态,IconFile 与 IconFilePressed 同路径保证按下不变样,最后 Update(true) 重绘。 行号列号靠索引算:GetRow 用 MathFloor(index/row_size)+1,GetCol 用 MathMod(index,row_size)+1。比如 row_size=5、index=12,行是第 3 行、列是第 3 列。 时间框架字符串转枚举别写一堆 if 也行,但原文用的就是逐条比对:"M1" 返 PERIOD_M1,一直到 "M4" 返 PERIOD_M4,后续可照此补 M5/M15 等。复制下面代码到你的 CProgram 类即可编译验证。

MQL5 / C++
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Set the icon                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CProgram::SetIcon(class="type">int index,class="type">int number)
{
  class=class="str">"cmt">//---
  class="type">class="kw">string image[]=
  {
    "Images\\EasyAndFastGUI\\Icons\\bmp16\\arrow_up.bmp",
    "Images\\EasyAndFastGUI\\Icons\\bmp16\\arrow_down.bmp"
  };
  class="type">class="kw">string path=(number>=class="num">0)?image[number]:"";
  if(number<class="num">0)
    m_signal_button[index].IsCenterText(true);
  else
    m_signal_button[index].IsCenterText(false);
  m_signal_button[index].IconFile(path);
  m_signal_button[index].IconFilePressed(path);
  m_signal_button[index].Update(true);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Set the tooltip                                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void CProgram::SetTooltip(class="type">int index,class="type">class="kw">string text="\n")
{
  m_signal_button[index].Tooltip(text);
  m_signal_button[index].ShowTooltip(true);
}
  class="type">int       GetRow(class="type">int index,class="type">int row_size);
  class="type">int       GetCol(class="type">int index,class="type">int row_size);
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Determining a row by the index                                          |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CProgram::GetRow(class="type">int index,class="type">int row_size)
{
  class="kw">return(class="type">int(MathFloor(index/row_size)+class="num">1));
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Determining a column by the index                                       |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">int CProgram::GetCol(class="type">int index,class="type">int row_size)
{
  class="kw">return(class="type">int(MathMod(index,row_size)+class="num">1));
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Return timeframe by row                                                 |
class=class="str">"cmt">//+------------------------------------------------------------------+
ENUM_TIMEFRAMES CProgram::StringToTimeframe(class="kw">const class="type">class="kw">string timeframe)
{
  if(timeframe=="M1")  class="kw">return(PERIOD_M1);
  if(timeframe=="M2")  class="kw">return(PERIOD_M2);
  if(timeframe=="M3")  class="kw">return(PERIOD_M3);
  if(timeframe=="M4")  class="kw">return(PERIOD_M4);

字符串周期到枚举的映射与表格定位

在 MT5 面板类工具里,用户看到的周期标签往往是 "M5""H4" 这类字符串,而底层调用 iMACD、iRSI 时必须喂 ENUM_TIMEFRAMES 枚举。下面这段映射把 21 种非常规周期(含 M6/M10/M12/M20/H2/H3/H6/H8/H12 等非标准档)逐一转成对应宏,最后兜底返回当前图表 Period(),避免空字符串直接崩句柄。 if(timeframe=="M5") return(PERIOD_M5); // 字符串M5命中即返回5分钟枚举 if(timeframe=="M6") return(PERIOD_M6); // M6非常规周期,MT5原生支持 if(timeframe=="M10") return(PERIOD_M10); // 10分钟线常用于剥头皮统计 if(timeframe=="M12") return(PERIOD_M12); if(timeframe=="M15") return(PERIOD_M15); if(timeframe=="M20") return(PERIOD_M20); if(timeframe=="M30") return(PERIOD_M30); if(timeframe=="H1") return(PERIOD_H1); if(timeframe=="H2") return(PERIOD_H2); // 2小时线在贵金属隔夜波动里有用 if(timeframe=="H3") return(PERIOD_H3); if(timeframe=="H4") return(PERIOD_H4); if(timeframe=="H6") return(PERIOD_H6); if(timeframe=="H8") return(PERIOD_H8); if(timeframe=="H12") return(PERIOD_H12); if(timeframe=="D1") return(PERIOD_D1); if(timeframe=="W1") return(PERIOD_W1); if(timeframe=="MN") return(PERIOD_MN1); //--- The default value return(::Period()); // 未匹配时退回图表自身周期,保证不中断 表格坐标反查靠两个函数:GetTimeframe 用 index 和 m_timeframes 大小算列,再取标签文本走上面的映射;GetSymbol 同理算行拿品种名。这样在 21 周期 × N 品种的矩阵面板里,点哪个格子都能还原出 (symbol, timeframe) 对。 GetSignal 接收还原后的 sy 与 tf,准备开指标句柄。外汇与贵金属杠杆高,周期映射错乱会直接让信号取样错位,实盘前务必在策略测试器用 EURUSD 的 H3 跑一遍确认返回的是 PERIOD_H3 而非默认周期。

MQL5 / C++
if(timeframe=="M5")  class="kw">return(PERIOD_M5);
if(timeframe=="M6")  class="kw">return(PERIOD_M6);
if(timeframe=="M10") class="kw">return(PERIOD_M10);
if(timeframe=="M12") class="kw">return(PERIOD_M12);
if(timeframe=="M15") class="kw">return(PERIOD_M15);
if(timeframe=="M20") class="kw">return(PERIOD_M20);
if(timeframe=="M30") class="kw">return(PERIOD_M30);
if(timeframe=="H1")  class="kw">return(PERIOD_H1);
if(timeframe=="H2")  class="kw">return(PERIOD_H2);
if(timeframe=="H3")  class="kw">return(PERIOD_H3);
if(timeframe=="H4")  class="kw">return(PERIOD_H4);
if(timeframe=="H6")  class="kw">return(PERIOD_H6);
if(timeframe=="H8")  class="kw">return(PERIOD_H8);
if(timeframe=="H12") class="kw">return(PERIOD_H12);
if(timeframe=="D1")  class="kw">return(PERIOD_D1);
if(timeframe=="W1")  class="kw">return(PERIOD_W1);
if(timeframe=="MN")  class="kw">return(PERIOD_MN1);
class=class="str">"cmt">//--- The class="kw">default value
class="kw">return(::Period());
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Determine the timeframe                                                            |
class=class="str">"cmt">//+------------------------------------------------------------------+
ENUM_TIMEFRAMES CProgram::GetTimeframe(class="type">int index)
{
  class="type">int tf=ArraySize(m_timeframes);
  class="kw">return(StringToTimeframe((m_timeframe_label[GetCol(index,tf)-class="num">1].LabelText())));
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Determine the symbol                                                              |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">class="kw">string CProgram::GetSymbol(class="type">int index)
{
  class="type">int tf=ArraySize(m_timeframes);
  class="kw">return(m_symbol_label[GetRow(index,tf)-class="num">1].LabelText());
}
class="type">bool                GetSignal(class="type">class="kw">string sy,ENUM_TIMEFRAMES tf,SIGNAL &signal_set);
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CProgram::GetSignal(class="type">class="kw">string sy,ENUM_TIMEFRAMES tf,SIGNAL &signal_set)
{
class=class="str">"cmt">//--- Getting the indicator handle
  class="type">int h=INVALID_HANDLE;

◍ 把信号配置映射成具体指标句柄

多品种扫描器要兼顾灵活与性能,常见做法是把「用哪个价格、用哪个指标」塞进一个 signal_set 结构,再在运行时翻译成 MT5 原生句柄。下面这段就是典型的映射层:先按 app_price 的 0~6 整数把收盘价、开盘价、最高价、最低价、中位数、典型价、加权价对应到 ENUM_APPLIED_PRICE,缺省落回 PRICE_CLOSE。 紧接着按 ind_type 的 0~6 去拿指标句柄:0 是 iATR、1 是 iCCI、2 是 iDeMarker、3 是 iForce(写死 MODE_SMA+VOLUME_TICK)、4 是 iWPR、5 是 iRSI、6 是 iMomentum;其中 CCI/RSI/Momentum 会吃前面解析出的 app_price,ATR/DeMarker/WPR 不依赖价格类型。 拿到句柄后立刻判 INVALID_HANDLE,失败就 Print 出品种名和 GetLastError() 并 return false,避免在后续 CopyBuffer 上踩坑。 真正取数只用 CopyBuffer(h,0,0,1,arr) 拉最新一根的缓冲区,若返回值不等于 1 同样报错退出;确认无误后调 IndicatorRelease(h) 释放句柄,再把 arr[0] 与 rule_value 比较前暂存到 m_ind_value。外汇与贵金属波动剧烈,这类动态建柄代码在跨周期扫描时可能频繁申请释放,建议在 MT5 策略测试器里用 EURUSD 的 M15 跑一遍,观察 Print 日志里句柄申请失败的频率。

MQL5 / C++
  ENUM_APPLIED_PRICE app_price;
  class="kw">switch(signal_set.app_price)
  {
  case  class="num">0:
      app_price=PRICE_CLOSE;
      class="kw">break;
  case  class="num">1:
      app_price=PRICE_OPEN;
      class="kw">break;
  case  class="num">2:
      app_price=PRICE_HIGH;
      class="kw">break;
  case  class="num">3:
      app_price=PRICE_LOW;
      class="kw">break;
  case  class="num">4:
      app_price=PRICE_MEDIAN;
      class="kw">break;
  case  class="num">5:
      app_price=PRICE_TYPICAL;
      class="kw">break;
  case  class="num">6:
      app_price=PRICE_WEIGHTED;
      class="kw">break;
  class="kw">default:
      app_price=PRICE_CLOSE;
      class="kw">break;
  }
class=class="str">"cmt">//---
  class="kw">switch(signal_set.ind_type)
  {
  case  class="num">0:
      h=iATR(sy,tf,signal_set.ind_period);
      class="kw">break;
  case  class="num">1:
      h=iCCI(sy,tf,signal_set.ind_period,app_price);
      class="kw">break;
  case  class="num">2:
      h=iDeMarker(sy,tf,signal_set.ind_period);
      class="kw">break;
  case  class="num">3:
      h=iForce(sy,tf,signal_set.ind_period,MODE_SMA,VOLUME_TICK);
      class="kw">break;
  case  class="num">4:
      h=iWPR(sy,tf,signal_set.ind_period);
      class="kw">break;
  case  class="num">5:
      h=iRSI(sy,tf,signal_set.ind_period,app_price);
      class="kw">break;
  case  class="num">6:
      h=iMomentum(sy,tf,signal_set.ind_period,app_price);
      class="kw">break;
  class="kw">default:
      class="kw">break;
  }
  if(h==INVALID_HANDLE)
  {
      Print(sy+". Failed to get handle");
      Print("Handle = ",h,"  error = ",GetLastError());
      class="kw">return(false);
  }
  class=class="str">"cmt">//---
  class="type">class="kw">double arr[class="num">1];
  if(CopyBuffer(h,class="num">0,
class="num">0,class="num">1,arr)!=class="num">1)
  {
      Print("sy= ",sy,"tf= ",EnumToString(tf)," Failed to get handle data ",GetLastError());
      class="kw">return(false);
  }
  IndicatorRelease(h);
class=class="str">"cmt">//--- Check the condition
  class="type">class="kw">double r_value=signal_set.rule_value;
  class="type">class="kw">double c_value=arr[class="num">0];
  m_ind_value=c_value;

「信号判定与时间框架过滤的实现细节」

在自定义信号引擎里,rule_type 用 0~4 五个整数编码了五类比较关系:大于、大于等于、等于、小于、小于等于。下面这段 switch 把当前值 c_value 和参考值 r_value 按规则比对,命中就给 s 赋 1,否则走 default 归零。 int s=0; switch(signal_set.rule_type) { case 0: if(c_value>r_value) s=1; break; case 1: if(c_value>=r_value) s=1; break; case 2: if(c_value==r_value) s=1; break; case 3: if(c_value<r_value) s=1; break; case 4: if(c_value<=r_value) s=1; break; default: s=0; break; } //--- if(s>0) return(true); return(false); 逐行看:s 初始化为 0;case 0 处理严格大于,case 1 放宽到含等号,case 2 只认相等,case 3 与 4 是小于方向的两种精度。任何未定义 rule_type 都落 default 返回 false,避免脏数据触发信号。 时间框架的白名单检查由 CheckTimeframe 完成,它在一个长度 21 的数组里挨个比对 tf_name,命中传入的 ENUM_TIMEFRAMES 才放行。实测若信号文件里配了非常规周期(如自设的 2 分钟图),而数组未覆盖,该函数会直接返回 false 导致信号沉默。 SearchSignals 先扫 Signal Monitor 目录下 signal_0.bin 到 signal_4.bin 共 5 个可能文件,用 FileIsExist 统计实际存在数量 cnt,再 ArrayResize 动态开数组。外汇与贵金属市场跳空频繁,这类基于本地 bin 文件的信号加载对文件写入时机敏感,实盘使用前建议在 MT5 策略测试器里跑一轮验证文件落盘逻辑。

MQL5 / C++
class="type">int s=class="num">0;
class="kw">switch(signal_set.rule_type)
{
 case  class="num">0:
   if(c_value>r_value)
     s=class="num">1;
   class="kw">break;
 case  class="num">1:
   if(c_value>=r_value)
     s=class="num">1;
   class="kw">break;
 case  class="num">2:
   if(c_value==r_value)
     s=class="num">1;
   class="kw">break;
 case  class="num">3:
   if(c_value<r_value)
     s=class="num">1;
   class="kw">break;
 case  class="num">4:
   if(c_value<=r_value)
     s=class="num">1;
   class="kw">break;
 class="kw">default:
   s=class="num">0;
   class="kw">break;
}
class=class="str">"cmt">//---
 if(s>class="num">0)
   class="kw">return(true);
 class="kw">return(false);
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CProgram::CheckTimeframe(ENUM_TIMEFRAMES tf,SIGNAL &signal_set)
{
  for(class="type">int i=class="num">0; i<class="num">21; i++)
  {
   if(StringToTimeframe(CharArrayToString(signal_set.tf_name[i].tf))==tf)
     class="kw">return(true);
  }
  class="kw">return(false);
}
class="type">bool                SearchSignals(class="type">void);
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                                                  |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">bool CProgram::SearchSignals(class="type">void)
{
class=class="str">"cmt">//--- Search for set signals
  SIGNAL signal_set[];
  class="type">int cnt=class="num">0;
  for(class="type">int i=class="num">0; i<class="num">5; i++)
  {
   if(FileIsExist("Signal Monitor\\signal_"+class="type">class="kw">string(i)+".bin"))
     cnt++;
  }
class=class="str">"cmt">//---
  ArrayResize(signal_set,cnt);
  ZeroMemory(signal_set);
class=class="str">"cmt">//---
  for(class="type">int i=class="num">0; i<cnt; i++)
  {
   class="type">int h=FileOpen("Signal Monitor\\signal_"+class="type">class="kw">string(i)+".bin",FILE_READ|FILE_BIN);

信号面板的渲染与窗口自适应

配置句柄拉到 INVALID_HANDLE 时直接弹窗报错并退出,这是面板初始化最常见的静默失败点——实盘里若没读到 signal_set 配置文件,后续按钮全空,必须靠 MessageBox 暴露出来。 下面这段逻辑负责把信号画到对应品种和周期的按钮上。先按 m_signal_button 数组长度遍历,取出每个格子的品种 sy 与周期 tf;若 CheckTimeframe 不通过就 continue 跳过,避免把 H1 信号误标到 M5 格。 GetSignal 命中后,按 label_type 决定标签内容是字符串还是指标值(DoubleToString 保留 3 位小数)。背景色、边框色、tooltip、图标依次设置;border_color 为 clrNONE 时回退用 back_color,图标为 false 时传 -1 清掉旧图标。 最后 AutoResize 用时间表头和品种表头的间隙加尺寸再加 5 像素算窗口大小,例如 XGap()+XSize()+5 决定宽。改完布局立刻调 SearchSignals() 重刷一遍,否则新尺寸下可能漏显信号。外汇与贵金属波动剧烈,面板仅作辅助提示,信号触发不代表行情必然朝该方向走。

MQL5 / C++
if(h==INVALID_HANDLE)
{
   MessageBox("Configuration not found","Signal Monitor");
   class="kw">return(false);
}
FileReadStruct(h,signal_set[i]);
FileClose(h);
for(class="type">int j=class="num">0;j<ArraySize(m_signal_button); j++)
{
   class=class="str">"cmt">//---
   class="type">class="kw">string sy=GetSymbol(j);
   ENUM_TIMEFRAMES tf=GetTimeframe(j);
   class=class="str">"cmt">//---
   if(!CheckTimeframe(tf,signal_set[i]))
      class="kw">continue;
   class=class="str">"cmt">//---
   if(GetSignal(sy,tf,signal_set[i]))
   {
      class=class="str">"cmt">//---
      if(signal_set[i].label_type==class="num">1)
         SetLabel(j,CharArrayToString(signal_set[i].label_value),signal_set[i].label_color);
      else
         SetLabel(j,DoubleToString(m_ind_value,class="num">3),signal_set[i].label_color);
      class=class="str">"cmt">//---
      if(signal_set[i].back_color!=clrNONE)
         SetBackground(j,signal_set[i].back_color);
      class=class="str">"cmt">//---
      if(signal_set[i].border_color!=clrNONE)
         SetBorderColor(j,signal_set[i].border_color);
      else
         SetBorderColor(j,signal_set[i].back_color);
      class=class="str">"cmt">//---
      if(signal_set[i].tooltip)
         SetTooltip(j,CharArrayToString(signal_set[i].tooltip_text));
      class=class="str">"cmt">//---
      if(signal_set[i].image)
         SetIcon(j,signal_set[i].img_index);
      else
         SetIcon(j,-class="num">1);
   }
}
class=class="str">"cmt">//--- Resize window
AutoResize(m_timeframe_label[tf-class="num">1].XGap()+m_timeframe_label[tf-class="num">1].XSize()+class="num">5,m_symbol_label[sy-class="num">1].YGap()+m_symbol_label[sy-class="num">1].YSize()+class="num">5);
class=class="str">"cmt">//---
SearchSignals();

◍ 用枚举控制信号刷新节奏

把刷新周期做成枚举,比写死延时更利于复盘不同周期下的信号密度。下面这段把 1 分、15 分、30 分、1 小时、4 小时映射成可切换的输入项,EA 启动时由 Update 参数决定走哪一支。 switch 里把各周期换算成秒:MINUTE 是 60,MINUTE_15 是 900,MINUTE_30 是 1800,HOUR 是 3600,HOUR_4 是 14400;default 兜底设 1 秒,避免漏配时直接卡死。update 用 TimeLocal()+cnts 算出下次触发时刻,OnTick 里只做一件事——本地时间越过 update 就调 SearchSignals() 并重置计时。 实盘接这逻辑时,外汇与贵金属波动受消息扰动大,高频刷新(如 MINUTE)可能让信号在低波动段频繁空转;4 小时档对趋势型策略更稳,但滞后概率也更高。开 MT5 把 Update 从 HOUR 切到 HOUR_4,观察 SearchSignals 调用次数在一个交易日的落差,就能直观摸到周期成本。

MQL5 / C++
enum TIMEFRAMMS
{
   MINUTE,                                                    class=class="str">"cmt">// class="num">1 Minute
   MINUTE_15,                                                 class=class="str">"cmt">// class="num">15 Minutes
   MINUTE_30,                                                 class=class="str">"cmt">// class="num">30 Minutes
   HOUR,                                                      class=class="str">"cmt">// class="num">1 Hour
   HOUR_4                                                     class=class="str">"cmt">// class="num">4 Hours
};
class="kw">input UPDATE                                                    Update          =   HOUR;                                                    class=class="str">"cmt">// Update interval
class="type">int cnts=class="num">0;
class="type">class="kw">datetime update;
class=class="str">"cmt">//---
   class="kw">switch(Update)
   {
   case MINUTE:
      cnts=class="num">60;
      class="kw">break;
   case MINUTE_15:
      cnts=class="num">60*class="num">15;
      class="kw">break;
   case MINUTE_30:
      cnts=class="num">60*class="num">30;
      class="kw">break;
   case HOUR:
      cnts=class="num">3600;
      class="kw">break;
   case HOUR_4:
      cnts=class="num">3600*class="num">4;
      class="kw">break;
   class="kw">default:
      cnts=class="num">1;
      class="kw">break;
   }
   update=TimeLocal()+cnts;
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//| Expert tick function                                             |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="type">void OnTick()
{
   if(TimeLocal()>update)
   {
      program.SearchSignals();
      update=TimeLocal()+cnts;
   }
}

「画得少,看得清」

把随附的 MQL5 文件夹直接丢进终端根目录就能跑,别手动去拼路径。在 MT5 里按 Ctrl+Shift+D 可一步跳到根目录下的 MQL5 文件夹,省得在文件管理器里翻层。 若账户里品种太多导致面板崩溃,进源码的 Program.mqh 把符号数上限改成市场观察里实际的数量即可,作者在原讨论里确认过这是超限的直接解法。 附带代码里那个 SAVE 结构用 72 长度的 bool 数组 tf 记录各时间帧状态,对应常见时间帧槽位,复制去改时注意别越界。外汇与贵金属波动剧烈,这类多品种监控工具只降低漏看概率,不消除跳空与滑点风险。

MQL5 / C++
class="kw">struct SAVE
{
  class="type">bool                tf[class="num">72];
};
让小布替你跑这套检索
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到信号命中与品种集状态,你只管决定跟不跟。把重复劳动交给小布,你专注决策。

常见问题

模板以项目内结构序列化到文件,具体路径跟随 MQL5 应用数据目录;迁移时连同项目文件一并拷贝即可,热键 S/L 不受影响。
编辑窗口走的是同一实例更新逻辑,保存即以新参数替换旧配置,不会叠加历史条件,撤销需重新加载旧模板。
目前小布盯盘的 AIGC 看板已内置跨品种信号诊断,品种集概念同源,但模板文件格式需经对应品种页手动映射,不支持原始文件直读。
算法按配置顺序遍历,不内置优先级;若想直盘先搜,保存模板时把直盘勾选排在前面即可。
只需扩展信号创建窗口的 GUI 元素与基类保存结构,搜索遍历部分无需重写,这是本篇分层设计的余量。