液态图表·进阶篇
「把液态图表写进独立窗口指标」
要在 MT5 里把「液态图表」跑起来,核心是写一个新指标,在副图窗口渲染,而不是覆盖主图。它支持三种位移模式:静态位移(SS)、动态开盘位移(DSO)、动态收盘位移(DSC),并且不靠参数对话框,直接用图表上的按钮切换模式和位移值。 输入参数里,Depth 决定副图展示多少根柱线,默认 100;BaseTF 是基准周期,默认 M1。inp_LC_mode 和 inp_LC_shift 只是初始值,运行时被 LC_mode 和 LC_shift 变量覆盖——按钮改的就是后者,这样不用重开参数面板。 RefreshBuffers() 是刷新缓存的发动机,OnCalculate 和 OnChartEvent 都会调它。函数先按模式给 shift 赋值:SS 模式直接取 LC_shift,DSO 给 -1,DSC 给 -2,再丢给 GetRatesLC() 算真实位移。算完把返回值写回 LC_shift 并调 refr_keys() 重绘按钮。 SS 模式下位移为 0 时,副图就是原始图表;把位移拉到 28,原本软弱的「轨道」会变成一根明显的「锤头」形态,这种结构在价格行为里倾向暗示做多机会,但外汇和贵金属波动剧烈,高杠杆下仅是概率信号,不是入场保证。 DSO 模式里新形成的柱线永远贴在最右;DSC 模式则保证柱线在基准周期时间间隔内即将收盘。三种模式切换逻辑和按钮绘制无关本文主题,但你要验证效果,直接挂 liquid_chart.mq5 即可。
<span class="keyword">input</span> <span class="macro">ENUM_TIMEFRAMES</span> BaseTF=<span class="macro">PERIOD_M1</span>; <span class="comment">class=class="str">"cmt">// LC Base Period</span> <span class="keyword">input</span> <span class="keyword">class="type">int</span> Depth=<span class="number">class="num">100</span>; <span class="comment">class=class="str">"cmt">// Depth, bars</span> <span class="keyword">input</span> ENUM_LC_MODE inp_LC_mode=LC_MODE_SS; <span class="comment">class=class="str">"cmt">// LC mode</span> <span class="keyword">input</span> <span class="keyword">class="type">int</span> inp_LC_shift=<span class="number">class="num">0</span>; <span class="comment">class=class="str">"cmt">// LC shift</span> <span class="keyword">enum</span> ENUM_LC_MODE {<span class="comment">class=class="str">"cmt">//plotting mode</span> LC_MODE_SS=<span class="number">class="num">0</span>, <span class="comment">class=class="str">"cmt">// <b>S</b>tatic <b>S</b>hift</span> LC_MODE_DSO=<span class="number">class="num">1</span>, <span class="comment">class=class="str">"cmt">// <b>D</b>ynamic <b>S</b>hift, just <b>O</b>pen</span> LC_MODE_DSC=<span class="number">class="num">2</span> <span class="comment">class=class="str">"cmt">// <b>D</b>ynamic <b>S</b>hift, expected <b>C</b>lose</span> }; <span class="keyword">class="type">bool</span> RefreshBuffers(<span class="keyword">class="type">int</span> total, <span class="keyword">class="type">class="kw">double</span> &buff1[], <span class="keyword">class="type">class="kw">double</span> &buff2[], <span class="keyword">class="type">class="kw">double</span> &buff3[], <span class="keyword">class="type">class="kw">double</span> &buff4[], <span class="keyword">class="type">class="kw">double</span> &col_buffer[]) { <span class="predefines">class="type">MqlRates</span> rates[]; <span class="functions">ArrayResize</span>(rates,Depth); <span class="comment">class=class="str">"cmt">//---</span> <span class="keyword">class="type">int</span> copied=<span class="number">class="num">0</span>; <span class="keyword">class="type">int</span> shift=<span class="number">class="num">0</span>; <span class="keyword">if</span>(LC_mode==LC_MODE_SS) shift = LC_shift;<span class="comment"> class=class="str">"cmt">//class="kw">static shift</span> <span class="keyword">else</span> <span class="keyword">if</span>(LC_mode==LC_MODE_DSO) shift = -<span class="number">class="num">1</span>; <span class="comment">class=class="str">"cmt">//calculate shift(beginning of the bar formation)</span> <span class="keyword">else</span> <span class="keyword">if</span>(LC_mode==LC_MODE_DSC) shift = -<span class="number">class="num">2</span>;<span class="comment"> class=class="str">"cmt">//calculate shift(end of the bar formation)</span> <span class="keyword">else</span> <span class="keyword">class="kw">return</span>(<span class="macro">false</span>); <span class="comment">class=class="str">"cmt">//---</span> copied=GetRatesLC(<span class="number">class="num">0</span>,Depth,rates,BaseTF,shift); <span class="comment">class=class="str">"cmt">//---</span> <span class="keyword">if</span>(copied<=<span class="number">class="num">0</span>) { <span class="functions">Print</span>(<span class="class="type">class="kw">string">"No data"</span>); <span class="keyword">class="kw">return</span>(<span class="macro">false</span>); } LC_shift = shift; refr_keys(); <span class="comment">class=class="str">"cmt">//--- initialize buffers with empty values</span> <span class="functions">ArrayInitialize</span>(buff1,<span class="number">class="num">0.0</span>); <span class="functions">ArrayInitialize</span>(buff2,<span class="number">class="num">0.0</span>); <span class="functions">ArrayInitialize</span>(buff3,<span class="number">class="num">0.0</span>); <span class="functions">ArrayInitialize</span>(buff4,<span class="number">class="num">0.0</span>); <span class="comment">class=class="str">"cmt">//---</span> <span class="keyword">class="type">int</span> buffer_index=total-copied; <span class="keyword">for</span>(<span class="keyword">class="type">int</span> i=<span class="number">class="num">0</span>;i<copied;i++) {
◍ 把K线四价灌进缓冲区并标阴阳
这段逻辑跑在自定义指标的 OnCalculate 里,逐根把当前柱的 open/high/low/close 写进四个独立缓冲区,方便后续在副图叠加或做形态判断。 rates[i].open<=rates[i].close 时给 col_buffer 打 1,代表阳线或十字;否则打 0 代表阴线。注意这里把开盘等于收盘也归为多头色,和 MT5 内置蜡烛图的 doji 着色习惯一致。 buffer_index++ 在循环尾推进,保证每根 K 对应唯一槽位;函数最终 return(true) 告知终端计算成功。外汇与贵金属波动大,这种着色仅反映已发生结构,不预示后续方向。
buff1[buffer_index]=rates[i].open;
buff2[buffer_index]=rates[i].high;
buff3[buffer_index]=rates[i].low;
buff4[buffer_index]=rates[i].close;
class=class="str">"cmt">//---
if(rates[i].open<=rates[i].close)
col_buffer[buffer_index]=class="num">1;class=class="str">"cmt">//bullish or doji
else
col_buffer[buffer_index]=class="num">0;class=class="str">"cmt">//bearish
class=class="str">"cmt">//
buffer_index++;
}
class=class="str">"cmt">//
class="kw">return(true);
}把均线EA和夹针EA改成可位移版本
以 MT5 自带的 Moving Average 例程(Experts\Examples\Moving Average)为基底,我们改出两个 EA:一个按均线交叉,一个按“夹针柱线”形态。两者都挂上 liquidchart.mqh,目的只有一个——在开启 LC_on 后,用 GetRatesLC() 替换标准 CopyRates(),从而把静态移位改成动态移位,方便后面比两种策略的优化差异。外汇和贵金属杠杆高,回测结论只代表历史概率,实盘可能明显偏离。 均线版新增了 LC_on、LC_shift、BaseTF 三个输入。LC_on 设为 true 且 LC_shift=0 时,结果应当和 LC_on=false 完全一致,这可以用来验证 GetRatesLC() 没接错。原版依赖指标句柄,改写后得手工算均价值,于是加了 CopyMABuffer():它返回 ma[] 里所需元素数,出错返回 0。 原版用即时报价成交量控开盘,我们没那数据,改成 newbar() 比前一根开盘时间。CheckForOpen() 和 CheckForClose() 里都调 newbar(),完整代码在 moving_average_lc.mq5。 夹针柱线(匹诺曹)由三根 K 线组成:中间 r[1] 是长影“鼻子”,两侧 r[0]、r[2] 是“眼睛”,极值不能越过中间影线。下行反转要求 r[0] 阳、r[2] 阴,A=r[0]最高、C=r[2]最高、B=r[1]最高,且 A 和 C 的最大值不超过 B;r[1] 实体不超过 inp 点数,影线不小于 inp 点数,影线/实体比不小于 inp 比值。形态在 r[3] 开盘时判定。 检测函数对历史数组要求至少 4 根:上夹针返回 1,下夹针返回 -1,无形态返回 0。策略极简——下夹针做空、上夹针做多,不带指标确认以保持实验纯粹。但严格参数下夹针极少出现,只靠反向夹针平仓容易踏空或扛亏,所以加了 inp_tp_pp、inp_sl_pp 止盈止损,设 0 就不挂。完整代码见 pinbar_lc.mq5,开 MT5 把这两个文件丢进 Experts 就能跑。
input class="type">class="kw">double MaximumRisk = class="num">0.1; class=class="str">"cmt">// Maximum Risk in percentage input class="type">class="kw">double DecreaseFactor = class="num">3; class=class="str">"cmt">// Decrease factor input class="type">int MovingPeriod = class="num">12; class=class="str">"cmt">// Moving Average period input class="type">int MovingShift = class="num">6; class=class="str">"cmt">// Moving Average shift input ENUM_TIMEFRAMES BaseTF=PERIOD_M1; class=class="str">"cmt">// LC Base Period input class="type">bool LC_on = true; class=class="str">"cmt">// LC mode ON input class="type">int LC_shift = class="num">0; class=class="str">"cmt">// LC shift class="type">int copied; if(LC_on==true) { class="type">int shift = LC_shift; copied=GetRatesLC(class="num">0,class="num">2,rt,BaseTF,shift); } else copied = CopyRates(_Symbol,_Period,class="num">0,class="num">2,rt); if(copied!=class="num">2) { Print("CopyRates of ",_Symbol," failed, no history"); class="kw">return; } class="type">int CopyMABuffer(class="type">int len,class="type">class="kw">double &ma[]) { if(len<=class="num">0) class="kw">return(class="num">0); class="type">MqlRates rates[]; class="type">int l=len-class="num">1+MovingPeriod; class="type">int copied; if(LC_on==true) { class="type">int shift = LC_shift; ArrayResize(rates,l); copied=GetRatesLC(MovingShift,l,rates,BaseTF,shift); } else copied=CopyRates(_Symbol,_Period,MovingShift,l,rates); class=class="str">"cmt">// if(copied<l) class="kw">return(class="num">0); class=class="str">"cmt">//
「上插针形态判定与K线遍历的裸逻辑」
这段逻辑把均线计算和 pin bar 识别拆成了两块独立函数,先说均线部分:外层循环跑 len 根 K 线,内层按 MovingPeriod 窗口累加 close 价,LC_on 开关决定是从头部还是尾部取序列,最终 ma[i] 存的是窗口均值。 newbar() 用静态变量 t_prev 卡住时间轴,只有出现新柱才放行后续判断,避免同根 K 线内重复触发——外汇和贵金属这种高波动品种,重复信号会直接放大滑点风险。 IsPinbar() 要求数组至少 4 根:索引 1 作为候选针杆,条件 r[0].open<r[0].close 且 r[2].open>r[2].close,且 r[1].high 必须高于两侧高点,这是典型上插针的「眼睛」结构。 针杆实体用 oc=MathAbs(open-close)/_Point 换算成点数,若超过 inp_pb_max_OC 直接弃;影线 shdw 低于 inp_pb_min_shdw 也弃;两者比例 shdw/oc 小于 inp_pb_min_ratio 同样不认。调这三个输入参数,就能在 MT5 里收紧或放宽形态过滤。
for(class="type">int i=class="num">0;i<len;i++) { class="type">class="kw">double sum=class="num">0; for(class="type">int j=class="num">0;j<MovingPeriod;j++) { if(LC_on==true) sum+=rates[j+i].close; else sum+=rates[copied-class="num">1-j-i].close; } ma[i]=sum/MovingPeriod; } class="kw">return(len); } if(rt[class="num">1].tick_volume>class="num">1) class="kw">return; class="type">bool newbar(class="type">class="kw">datetime t) { class="kw">static class="type">class="kw">datetime t_prev=class="num">0; if(t!=t_prev) { t_prev=t; class="kw">return(true); } class="kw">return(false); } if(newbar(rt[class="num">1].time)==false) class="kw">return; if(r[class="num">0].open<r[class="num">0].close && r[class="num">2].open>r[class="num">2].close && r[class="num">1].high>MathMax(r[class="num">0].high,r[class="num">2].high)) { class=class="str">"cmt">//--- eyes of the upper pin bar class="type">class="kw">double oc=MathAbs(r[class="num">1].open-r[class="num">1].close)/_Point; if(oc>inp_pb_max_OC) class="kw">return(class="num">0); class="type">class="kw">double shdw=(r[class="num">1].high-MathMax(r[class="num">1].open,r[class="num">1].close))/_Point; if(shdw<inp_pb_min_shdw) class="kw">return(class="num">0); if(oc!=class="num">0) { if((shdw/oc)<inp_pb_min_ratio) class="kw">return(class="num">0); } class="kw">return(class="num">1); } class="type">int IsPinbar(class="type">MqlRates &r[]) { class=class="str">"cmt">//--- there must be class="num">4 values in the r[] array if(ArraySize(r)<class="num">4) class="kw">return(class="num">0); if(r[class="num">0].open<r[class="num">0].close && r[class="num">2].open>r[class="num">2].close && r[class="num">1].high>MathMax(r[class="num">0].high,r[class="num">2].high)) {class=class="str">"cmt">//--- eyes of the upper pin bar class="type">class="kw">double oc=MathAbs(r[class="num">1].open-r[class="num">1].close)/_Point; if(oc>inp_pb_max_OC) class="kw">return(class="num">0); class="type">class="kw">double shdw=(r[class="num">1].high-MathMax(r[class="num">1].open,r[class="num">1].close))/_Point; if(shdw<inp_pb_min_shdw) class="kw">return(class="num">0); if(oc!=class="num">0) {
◍ 下插针的判定与信号触发逻辑
上一段处理了上 pin bar 的返回 1,这里接着看下插针分支:当最新 K 线为阴线、倒数第三根 K 线为阳线、且中间那根 K 线的最低价低于左右两根的最低价时,才进入下插针判定。 代码先算中间 K 线的实体点数 oc,若 oc 超过 inp_pb_max_OC(默认 20 点)直接淘汰;再算下方影线 shdw = 实体下沿到最低价的点数,若小于 inp_pb_min_shdw(默认 40 点)也淘汰。 若实体不为 0,还要求 shdw/oc 不小于 inp_pb_min_ratio(默认 2.0),否则返回 0。全部通过则返回 -1,代表下插针成立,对应做多倾向。 外部输入参数写死为:inp_pb_min_shdw=40、inp_pb_max_OC=20、inp_pb_min_ratio=2.0,三者共同框定针杆比例。实盘里把 40 调到 25,可能更容易抓到小周期黄金的刺透形态,但假信号概率会上升。 历史抓取用 CopyRates 取 4 根 K 线,若 copied!=4 就打印失败并返回;只有 newbar 为真才跑 IsPinbar。pb==1 给卖信号,pb==-1 给买信号,已有同向持仓时 signal 置 true。外汇与贵金属波动剧烈,pin bar 仅作概率参考,开 MT5 调参验证前先想清点差与滑点。
else if(r[class="num">0].open>r[class="num">0].close && r[class="num">2].open<r[class="num">2].close && r[class="num">1].low<MathMin(r[class="num">0].low,r[class="num">2].low)) { class=class="str">"cmt">//--- eyes of the lower pin bar class="type">class="kw">double oc=MathAbs(r[class="num">1].open-r[class="num">1].close)/_Point; if(oc>inp_pb_max_OC) class="kw">return(class="num">0); class="type">class="kw">double shdw=(MathMin(r[class="num">1].open,r[class="num">1].close)-r[class="num">1].low)/_Point; if(shdw<inp_pb_min_shdw) class="kw">return(class="num">0); if(oc!=class="num">0) { if((shdw/oc)<inp_pb_min_ratio) class="kw">return(class="num">0); } class="kw">return(-class="num">1); } class="kw">return(class="num">0); } input class="type">uint inp_pb_min_shdw = class="num">40; class=class="str">"cmt">// Pin bar min shadow, point input class="type">uint inp_pb_max_OC = class="num">20; class=class="str">"cmt">// Pin bar max OC, point input class="type">class="kw">double inp_pb_min_ratio = class="num">2.0; class=class="str">"cmt">// Pin bar shadow to OC min ratio class="type">int copied; if(LC_on==true) { class="type">int shift = LC_shift; copied=GetRatesLC(class="num">0,class="num">4,rt,BaseTF,shift); } else copied = CopyRates(_Symbol,_Period,class="num">0,class="num">4,rt); if(copied!=class="num">4) { Print("CopyRates of ",_Symbol," failed, no history"); class="kw">return; } if(newbar(rt[class="num">3].time)==false) class="kw">return; class="type">int pb=IsPinbar(rt); if(pb==class="num">1) class=class="str">"cmt">// upper pin bar signal=ORDER_TYPE_SELL; class=class="str">"cmt">// sell conditions else if(pb==-class="num">1) class=class="str">"cmt">// lower pin bar signal=ORDER_TYPE_BUY; class=class="str">"cmt">// buy conditions if(type==(class="type">long)POSITION_TYPE_BUY && pb==class="num">1) signal=true; if(type==(class="type">long)POSITION_TYPE_SELL && pb==-class="num">1) signal=true; class="type">class="kw">double sl=class="num">0,tp=class="num">0,p=class="num">0; class="type">class="kw">double ask=SymbolInfoDouble(_Symbol,SYMBOL_ASK);
挂单价格与止损止盈的双向换算
这段逻辑解决了一个实操痛点:信号方向不同,开仓价、止损价、止盈价的基准和加减方向完全反过来。做空时以 bid 为基准,止损放在 ask 上方、止盈在 ask 下方;做多则反过来用 ask 基准、止损在 bid 下方、止盈在 bid 上方。 代码里 inp_sl_pp 和 inp_tp_pp 是以点数(_Point)为单位的偏移量,经 NormalizeDouble 按品种精度 _Digits 规整后才写入。注意原文做空分支的止盈用了 inp_sl_pp 而非 inp_tp_pp,这大概率是笔误,实盘复制时建议改成对应变量,否则空单止损止盈会对称重叠。 外汇与贵金属杠杆高,点值跳空可能让 sl/tp 失效,上述换算只保证报价逻辑自洽,不预示任何胜率。开 MT5 把这段塞进 EA 的 OrderSend 前处理块,改完变量名跑一次回测就能看出差异。
class="type">class="kw">double bid=SymbolInfoDouble(_Symbol,SYMBOL_BID); if(signal==ORDER_TYPE_SELL) { p=bid; if(inp_sl_pp!=class="num">0) sl=NormalizeDouble(ask+inp_sl_pp*_Point,_Digits); if(inp_tp_pp!=class="num">0) tp=NormalizeDouble(ask-inp_sl_pp*_Point,_Digits); } else { p=ask; if(inp_sl_pp!=class="num">0) sl=NormalizeDouble(bid-inp_sl_pp*_Point,_Digits); if(inp_tp_pp!=class="num">0) tp=NormalizeDouble(bid+inp_sl_pp*_Point,_Digits); } CTrade trade; trade.PositionOpen(_Symbol,signal,TradeSizeOptimized(),p,sl,tp);