手工制图表和交易工具箱(第三部分)。 优化和全新工具·进阶篇
(2/3)·从智能交易系统改指标后,40 个图表同开也不再卡顿,这篇讲清重构与新增工具
很多交易者把键盘快捷键脚本挂在 EA 上,却没意识到多图表同时跑键盘检查会悄悄拖慢终端。当选项卡开到三四十个,击键响应变钝,制图节奏直接被打断。其实只需让非活动图表闭嘴,就能把负载压回单窗口。
◍ 用鼠标点图拉出一段自由趋势线
在 MT5 自定义指标里,想靠鼠标点两下就把一段趋势线画出来,核心落在这个 DrawFreeLine 函数。它不负责画整条线,只管「找极值点 + 记录第一击坐标 + 清掉旧标记」,真正的连线逻辑在别处。 函数入口四个参数:_bar 是搜索起点柱序,_isUp 决定找顶还是找底,_fractalSizeRight 和 _fractalSizeLeft 默认都是 1,代表极值左右各比对 1 根 K 线。fractalForFirstSearch 用 MathMax 取二者较大值再乘 2,给首次环绕搜索留了缓冲柱数。 第一次点击时 m_Clicks_Count 为 0,代码把它置 1,并按 _isUp 用 iHigh 或 iLow 抓 selectedBar 的 prices,时间轴用 iTime 取。之后还调了 DeepPointSearch 往更高精度时间层蹭一下极值时间,最后 DrawFirstPointMarker 画个起点标记。 第二次点击走 else 分支:先 ObjectDelete 删掉第一次的标记物,再按同样逻辑取 countedPrice / countedTime。到这里两段坐标齐了,外汇和贵金属波动大、滑点频繁,这种手动锚点方式在 M5 以下周期可能偏滞后,建议开 MT5 用 EURUSD 的 H1 实测两次点击的偏移量。
class=class="str">"cmt">//| Parameters: | class=class="str">"cmt">//| class="type">int _bar - bar to start search at | class=class="str">"cmt">//| class="type">bool _isUp - top or bottom? | class=class="str">"cmt">//| class="type">int _fractalSizeRight - number of bars to the right of extr | class=class="str">"cmt">//| class="type">int _fractalSizeLeft - number of bars to the left of extremum| class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CGraphics::DrawFreeLine( class="type">int _bar, class="type">bool _isUp, class="type">int _fractalSizeRight=class="num">1, class="type">int _fractalSizeLeft=class="num">1 ) { class=class="str">"cmt">//--- Variables class="type">class="kw">double selectedPrice,countedPrice,trendPrice1,trendPrice2; class="type">class="kw">datetime selectedTime,countedTime,trendTime1,trendTime2; class="type">int selectedBar,countedBar; class="type">int bar1,bar2; class="type">class="kw">string trendName="",trendDescription="p2;"; class="type">int fractalForFirstSearch = MathMax(_fractalSizeRight,_fractalSizeLeft)* class="num">2; class=class="str">"cmt">//--- Search for a bar that meets the extremum criteria selectedBar = CUtilites::GetNearesExtremumSearchAround( _bar, _isUp, _fractalSizeLeft, _fractalSizeRight ); class=class="str">"cmt">//--- Building the starting marker if(class="num">0==m_Clicks_Count) { m_Clicks_Count=class="num">1; if(_isUp) { m_First_Point_Price=iHigh(NULL,PERIOD_CURRENT,selectedBar); } else { m_First_Point_Price=iLow(NULL,PERIOD_CURRENT,selectedBar); } m_First_Point_Time=iTime(NULL,PERIOD_CURRENT,selectedBar); class=class="str">"cmt">//--- m_First_Point_Time=CUtilites::DeepPointSearch( m_First_Point_Time, _isUp, ENUM_TIMEFRAMES(Period()) ); class=class="str">"cmt">//--- DrawFirstPointMarker(_isUp); } class=class="str">"cmt">//--- Processing a click on the chart else { ObjectDelete(class="num">0,m_First_Point_Marker_Name); if(_isUp) { countedPrice=iHigh(NULL,PERIOD_CURRENT,selectedBar); } else { countedPrice=iLow(NULL,PERIOD_CURRENT,selectedBar); } countedTime=iTime(NULL,PERIOD_CURRENT,selectedBar);
「在小周期上把趋势线画正」
这段代码干的事很直接:把在大周期找到的拐点,映射回当前图表的小周期坐标,再创建一条向右延伸的趋势线。外汇和贵金属这类高杠杆品种,跨周期画线若时间锚点错位,视觉上就会出现明显偏移,动手前建议先在 EURUSD 的 M15 上跑一遍。 先看时间锚点的处理。DeepPointSearch 把 countedTime 在小周期里向下深挖,保证拐点落在真实存在的 K 线上;随后用 if(countedTime<m_First_Point_Time) 强制让趋势线总是从左画到右——如果你习惯从右往左看结构,可以把这一段到下一个注释之间的代码注释掉。 TrendCreate 的调用里,颜色由 GetTimeFrameColor(GetAllLowerTimeframes()) 自动按小周期取,射线参数写死为 true(向右无限延伸)。线宽和线型走的是外部变量 Trend_Line_Width 与 Trend_Line_Style,想改观感直接调这两个全局量即可。 末尾用 iBarShift 把两个时间点转成 bar 索引,再用 GetTimeInFuture 按 (bar1-bar2)*m_Free_Trend_Length_Coefficient 推算线右端延伸到的未来 bar 位置。系数 m_Free_Trend_Length_Coefficient 设成 1.5 时,线长约为两拐点间距的 1.5 倍,可自行改了看延展是否顺眼。
class=class="str">"cmt">//--- Move a point in time on smaller timeframes countedTime=CUtilites::DeepPointSearch(countedTime,_isUp,ENUM_TIMEFRAMES(Period())); class=class="str">"cmt">//--- The line is always drawn from left to right. class=class="str">"cmt">//--- If it is not convenient, you can comment this part class=class="str">"cmt">//--- up to the next comment if(countedTime<m_First_Point_Time) { trendTime1=countedTime; trendPrice1=countedPrice; trendTime2=m_First_Point_Time; trendPrice2=m_First_Point_Price; } else { trendTime2=countedTime; trendPrice2=countedPrice; trendTime1=m_First_Point_Time; trendPrice1=m_First_Point_Price; } class=class="str">"cmt">//--- Set the description for future correction trendDescription+=TimeToString(trendTime2)+";"+DoubleToString(trendPrice2,Digits()); class=class="str">"cmt">//selectedPrice=CUtilites::EquationDirect( class=class="str">"cmt">// trendTime1,trendPrice1,trendTime2,trendPrice2,selectedTime class=class="str">"cmt">// ); trendName=CUtilites::GetCurrentObjectName(allPrefixes[class="num">0],OBJ_TREND); TrendCreate( class="num">0, class=class="str">"cmt">// Chart ID trendName, class=class="str">"cmt">// Line name class="num">0, class=class="str">"cmt">// Subwindow number trendTime1, class=class="str">"cmt">// time of the first point trendPrice1, class=class="str">"cmt">// price of the first point trendTime2, class=class="str">"cmt">// time of the second point trendPrice2, class=class="str">"cmt">// price of the second point CUtilites::GetTimeFrameColor( CUtilites::GetAllLowerTimeframes() ), class=class="str">"cmt">// line class="type">class="kw">color Trend_Line_Style, class=class="str">"cmt">// line style Trend_Line_Width, class=class="str">"cmt">// line width class="kw">false, class=class="str">"cmt">// background object true, class=class="str">"cmt">// is the line selected true class=class="str">"cmt">// ray to the right ); bar1=iBarShift(NULL,class="num">0,trendTime1); bar2=iBarShift(NULL,class="num">0,trendTime2); selectedTime = CUtilites::GetTimeInFuture( class=class="str">"cmt">//iTime(NULL,PERIOD_CURRENT,class="num">0), trendTime1, (class="type">int)((bar1-bar2)*m_Free_Trend_Length_Coefficient), COUNT_IN_BARS
趋势线拖拽后的射线与文本落点
手动拉完趋势线后,代码会把端点时间通过 GetTimeInFuture 推到未来,再取该时刻的价位回填。其中 m_Free_Trend_Length_Coefficient 控制延伸长度,按 (bar1-bar2)*系数 算出的柱数决定射线右端落在第几根 K 线。 ObjectSetInteger 连设 OBJPROP_RAY 与 OBJPROP_RAY_RIGHT,两条都等于 IsRay() 的返回值,意味着左右射线开关被同步绑定到同一个函数结果。若 IsRay() 返回 false,画出来就是段线而非无限延伸线。 ObjectMove 用 selectedTime 和 selectedPrice 把第二点挪到未来位置,随后 m_Clicks_Count 清零并 ToggleFreeLineMode 退出自由绘制。最后 ObjectSetString 写 trendDescription 文本、ChartRedraw 重绘——少一步都可能看到旧标签残留。 开 MT5 把这段塞进你自己的画线脚本,改 m_Free_Trend_Length_Coefficient 从 1.5 调到 3.0,能直接对比射线右端外抛距离的变化。外汇与贵金属杠杆高,脚本仅辅助标线,不构成方向判断。
selectedTime = CUtilites::GetTimeInFuture( class=class="str">"cmt">//iTime(NULL,PERIOD_CURRENT,class="num">0), trendTime1, (class="type">int)((bar1-bar2)*m_Free_Trend_Length_Coefficient), COUNT_IN_BARS ); selectedPrice= ObjectGetValueByTime(class="num">0,trendName,selectedTime); m_Clicks_Count=class="num">0; ToggleFreeLineMode();
◍ 下钻找点的循环逻辑
画线工具里每个端点都要单独跑一次 DeepPointSearch,这个函数放在实用工具文件里,核心任务是把高时间帧上的某个时间点,下钻到尽可能低的时间帧去定位最精确的日期。 最绕的部分是主搜索片段怎么判定历史里到底有没有目标时间。低周期往往缺高周期的信息,光用 iBars 数柱线不够,因为终端能显示的柱线数有上限。代码里先用 TerminalInfoInteger(TERMINAL_MAXBARS) 拿到这个上限,若 iBars 返回值超过上限就砍到 terminalMaxBars-1,避免越界读历史。 接着用 iTime 取当前周期最老一根柱的时间。若它比_neededTime 还早,说明目标时间在该周期历史范围内,可能已触到最深层;若最老时间晚于目标,则高周期数据更全,直接跳上一层周期继续找。 所有检查通过后,才在更高周期那根烛条范围内找极端点。用 iBarShift 锁定高周期柱号,再按 PeriodSeconds 比值推算低周期应扫多少根柱,配合标准极值函数定出高低点,反推时间收工。 当前实现只支持 T 键和 Q 键调用的指标线,外汇与贵金属波动大、滑点频繁,这类跨周期下钻在高波动时段可能偏位,实盘前建议在 MT5 用历史数据跑一遍校验。下个版本计划放开到全部品种并做单独定制。
<span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| Search for a given point on lower timeframes |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| Parameters: |</span> <span class="comment">class=class="str">"cmt">//| class="type">class="kw">datetime _neededTime - start time on a higher timeframe |</span> <span class="comment">class=class="str">"cmt">//| class="type">bool _isUp - search by highs or by lows |</span> <span class="comment">class=class="str">"cmt">//| ENUM_TIMEFRAMES _higher_TF - the highest period |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="comment">class=class="str">"cmt">//| Return value: |</span> <span class="comment">class=class="str">"cmt">//| More accurate date(on the lowest possible timeframe) |</span> <span class="comment">class=class="str">"cmt">//+------------------------------------------------------------------+</span> <span class="keyword">class="type">class="kw">datetime</span> CUtilites::DeepPointSearch( <span class="keyword">class="type">class="kw">datetime</span> _neededTime, <span class="keyword">class="type">bool</span> _isUp, <span class="macro">ENUM_TIMEFRAMES</span> _higher_TF=<span class="macro">PERIOD_CURRENT</span> ) { <span class="comment">class=class="str">"cmt">//---</span> <span class="comment">class=class="str">"cmt">//--- As a result it gets the most accurate time available</span> <span class="keyword">class="type">class="kw">datetime</span> deepTime=<span class="number">class="num">0</span>; <span class="comment">class=class="str">"cmt">//--- current timeframe</span> <span class="macro">ENUM_TIMEFRAMES</span> currentTF; <span class="comment">class=class="str">"cmt">//--- The number of the highest timeframe in the list of all available periods</span> <span class="keyword">class="type">int</span> highTFIndex = GetTimeFrameIndexByPeriod(_higher_TF); <span class="comment">class=class="str">"cmt">//--- The higher period in seconds</span> <span class="keyword">class="type">int</span> highTFSeconds = <span class="functions">PeriodSeconds</span>(_higher_TF); <span class="comment">class=class="str">"cmt">//--- Current interval in seconds</span> <span class="keyword">class="type">int</span> currentTFSeconds; <span class="comment">class=class="str">"cmt">//--- Counter</span> <span class="keyword">class="type">int</span> i; <span class="comment">class=class="str">"cmt">//--- Bar number on a higher timeframe</span> <span class="keyword">class="type">int</span> highBar=<span class="functions">iBarShift</span>(<span class="macro">NULL</span>,_higher_TF,_neededTime); <span class="comment">class=class="str">"cmt">//--- Bar number on the current timeframe</span> <span class="keyword">class="type">int</span> currentBar; <span class="comment">class=class="str">"cmt">//--- The total number of bars on the current timeframe</span> <span class="keyword">class="type">int</span> tfBarsCount; <span class="comment">class=class="str">"cmt">//--- How many bars of a lower TF fit into one bar of a higher TF</span> <span class="keyword">class="type">int</span> lowerBarsInHigherPeriod; <span class="comment">class=class="str">"cmt">//--- Maximum allowed number of bars in the terminal</span> <span class="keyword">class="type">int</span> terminalMaxBars = <span class="functions">TerminalInfoInteger</span>(<span class="macro">TERMINAL_MAXBARS</span>); <span class="comment">class=class="str">"cmt">//--- Loop sequentially through all timeframes</span> <span class="keyword">for</span>(i=<span class="number">class="num">0</span>; i<highTFIndex; i++) { <span class="comment">class=class="str">"cmt">//--- Get a timeframe by a number in the list</span> currentTF=GetTimeFrameByIndex(i); <span class="comment">class=class="str">"cmt">//--- Check if this timeframe has the required time.</span> <span style="background-class="type">class="kw">color:rgb(class="num">216, class="num">232, class="num">194);">tfBarsCount=<span class="functions">iBars</span>(<span class="macro">NULL</span>,currentTF); <span class="keyword">if</span>(tfBarsCount>terminalMaxBars-<span class="number">class="num">1</span>) { tfBarsCount=terminalMaxBars-<span class="number">class="num">1</span>; } deepTime=<span class="functions">iTime</span>(<span class="macro">NULL</span>,currentTF,tfBarsCount-<span class="number">class="num">1</span>); <span class="comment">class=class="str">"cmt">//--- If it has, find it.</span> <span class="keyword">if</span>(deepTime><span class="number">class="num">0</span> && deepTime<_neededTime)</span> {
「跨周期定位极值棒的检索逻辑」
在多周期嵌套扫描里,先拿 PeriodSeconds 算出当前周期秒数,再用高周期秒数除以它得到 lowerBarsInHigherPeriod,也就是一根高周期 K 线覆盖了多少根低周期棒。这个值直接决定了后续 iHighest / iLowest 的搜索窗口宽度。 若标记了向上方向 _isUp,就在高周期上用 iHighest 找 MODE_HIGH 的极值棒;否则用 iLowest 找 MODE_LOW。搜索范围写成 currentBar-lowerBarsInHigherPeriod+1 到 currentBar 之后多取一根(lowerBarsInHigherPeriod+1),确保把高周期那一根完整覆盖的低周期区间都扫进去。 循环里一旦通过 iTime 拿到 deepTime 就 break 退出;若一直走到高周期索引 highTFIndex 才结束,说明目标时间只存在于更高周期,此时直接把 _neededTime 赋给 deepTime 返回。 终端层面还有一道硬限制:TerminalInfoInteger(TERMINAL_MAXBARS) 给出当前 MT5 客户端允许的最大棒数,跨周期回看深度不能超过它,否则 iBarShift 会返回 -1,定位直接失效。外汇与贵金属杠杆品种波动剧烈,这类跨周期计算务必先在策略测试器里用真实品种复盘验证。
currentTFSeconds=PeriodSeconds(currentTF); class=class="str">"cmt">//--- Search for the required bar only within the higher TF candlestick lowerBarsInHigherPeriod=highTFSeconds/currentTFSeconds; currentBar = iBarShift(NULL,currentTF,_neededTime); if(_isUp) { currentBar = iHighest( NULL,currentTF,MODE_HIGH, lowerBarsInHigherPeriod+class="num">1, currentBar-lowerBarsInHigherPeriod+class="num">1 ); } else { currentBar = iLowest( NULL,currentTF,MODE_LOW, lowerBarsInHigherPeriod+class="num">1, currentBar-lowerBarsInHigherPeriod+class="num">1 ); } deepTime=iTime(NULL,currentTF,currentBar); class=class="str">"cmt">//--- Once the required time is found, stop the search break; } } class=class="str">"cmt">//--- If reached the end of the loop if(i==highTFIndex) { class=class="str">"cmt">//--- then the required time is only available on the higher timeframe. deepTime=_neededTime; } class=class="str">"cmt">//--- class="kw">return (deepTime); } class=class="str">"cmt">//--- Maximum allowed number of bars in the terminal class="type">int terminalMaxBars = TerminalInfoInteger(TERMINAL_MAXBARS);
用描述字段兜住时间缺口
指标线画出去之后,最怕的是标的出现时间跳空:线尾端和中间参考框差出一天多,末端被迫偏移,顶部附近的直线走向也跟着歪。线一缩窄,新的突破就冒出来,策略信号可能因此失真。外汇市场常一周才出现一次峰值跳空,感受不明显;股票端随交易所不同,一天内可能多次发生时间缺口,靠肉眼逐个修线不现实。 思路是把“正确坐标”先藏起来,再按实际缺口回推。我选了直线的描述(OBJPROP_TEXT)来存,因为自动画对象时交易者大多不填描述,不会撞车;线多的话也可改用清单文件或终端全局变量。下面这段把基准点时间价格拼进描述字符串。 [CODE] /* Graphics.mqh */ void CGraphics::DrawFreeLine(//...) { //... string trendDescription="p2;"; //... trendDescription+=TimeToString(trendTime2)+";"+DoubleToString(trendPrice2,Digits()); //... ObjectSetString(0,trendName,OBJPROP_TEXT,trendDescription); [/CODE] 真正校正放在每小时开头跑一次。核心函数先查对象在不在,再拆描述文本,只认以“p2”起头、至少3段的记录;基准点时间从描述第2段还原。股票的高频时间缺口会让末端漂移,外汇和贵金属虽跳空少但杠杆高、风险大,这类校正逻辑在实盘前务必用策略 tester 跑一遍验证。
class=class="str">"cmt">/* Graphics.mqh */ class="type">void CGraphics::DrawFreeLine(class=class="str">"cmt">//...) { class=class="str">"cmt">//... class="type">class="kw">string trendDescription="p2;"; class=class="str">"cmt">//... trendDescription+=TimeToString(trendTime2)+";"+DoubleToString(trendPrice2,Digits()); class=class="str">"cmt">//... ObjectSetString(class="num">0,trendName,OBJPROP_TEXT,trendDescription); class=class="str">"cmt">/* Utilites.mqh */ class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Adjusts the position of line end in the future in case of price | class=class="str">"cmt">//| gaps | class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Parameters: | class=class="str">"cmt">//| class="type">class="kw">string _line_name - the name of the line to be corrected | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void CUtilites::CorrectTrendFutureEnd(class="type">class="kw">string _line_name) { class=class="str">"cmt">//--- if(ObjectFind(class="num">0,_line_name)<class="num">0) { PrintDebugMessage(__FUNCTION__+" _line_name="+_line_name+": Object does not exist"); class=class="str">"cmt">//--- If there is no object to search, there is nothing more to do. class="kw">return; } class=class="str">"cmt">//--- Get a description class="type">class="kw">string line_text=ObjectGetString(class="num">0,_line_name,OBJPROP_TEXT); class="type">class="kw">string point_components[]; class=class="str">"cmt">// array for point description fragments class="type">class="kw">string name_components[]; class=class="str">"cmt">// array containing line name fragments class="type">class="kw">string helpful_name="Helpful line"; class=class="str">"cmt">// the name of the auxiliary line class="type">class="kw">string vertical_name=""; class=class="str">"cmt">// the name of the corresponding vertical from the crosshair class=class="str">"cmt">//--- Get the point time and price in class="type">class="kw">string form class="type">int point_components_count=StringSplit(line_text,StringGetCharacter(";",class="num">0),point_components); class="type">class="kw">datetime time_of_base_point; class=class="str">"cmt">// time of the basic point class="type">class="kw">datetime time_first_point,time_second_point; class=class="str">"cmt">// the time of the first and the second point class="type">class="kw">datetime time_far_ideal; class=class="str">"cmt">// estimated time in the future class="type">class="kw">double price_of_base_point; class=class="str">"cmt">// the price of the basic point class="type">class="kw">double price_first_point,price_second_point; class=class="str">"cmt">// the prices of the first and the second point class="type">int i; class=class="str">"cmt">// counter class=class="str">"cmt">//--- Check if the line is needed if(line_text=="" || point_components_count<class="num">3 || point_components[class="num">0]!="p2") { PrintDebugMessage(__FUNCTION__+" Error: the line cannot be used"); class="kw">return; } class=class="str">"cmt">//--- Get the coordinates of the "basic" point from the line description time_of_base_point=StringToTime(point_components[class="num">1]);