开发和分析交易系统的最佳方法·综合运用
(3/3)·很多人把 EA 写越长越当护身符,结果 2000 行空转、50 行反而跨货币稳跑,差在哪?
「柱形里到底存了哪六个值」
在 MT5 里,一根柱(烛形)不是把这段时间所有 tick 都留下来,而是只压成六个字段:Open、Close、High、Low、Time、Volume。前四个是价格点,Time 是柱开启时刻,类型是 datetime——本质是 1970年1月1日 起算的秒数;Volume 在这套数据模型里等于该柱内的报价点数量,不是成交手数。 价格四个值是核心,但 Time 和 Volume 不能丢。比如跨会话的柱,Time 跳变本身就能暴露流动性断层;Volume 异常放大往往先于形态破位。能把这些字段对齐处理,信号才有底。 柱和烛形指的是同一组数据,差别只在画法。纯裸 K 形态学别当圣经,它只是图形数据,单独用容易骗线;接上指标或其他方法交叉验证,命中概率才站得住。外汇和贵金属杠杆高,这类结构信号失效也快,验证前先想好止损位。
用成交量与时间窗搭一个轻量EA
成交量激增往往暴露出大资金的关键决策点,而限制EA只在特定服务器时段内交易,能让我们聚焦那些成交量真正有信息量的价格区域。这一节直接落一个可改写的EA骨架:以柱内成交量、收盘价位移和线性衰减为权重,把最近50根柱(CandlesE=50)压缩成一个综合信号,再按信号强度与单边占比开平仓。 模式枚举只留四种(MODE_1到MODE_4),作者习惯不超过四种变体,避免假设正确时反而看不清哪个方程最贴市场。前两种权重不向分析区远端衰减,后两种是带线性衰减的副本——衰减越简单越省算力,先试线性的够用。 输入块里时间窗由TradeHour=0/ TradeMinute=1 到 TradeHourEnd=23/ TradeMinuteEnd=59 框定;Signal=200是信号功率门槛,PercentE=52意味着某侧信号占比过半才倾向触发。SLE与TPE都设3000点,Lot=0.01,MagicF=15670867,外汇与贵金属杠杆品种下这些默认值请先模拟盘验证,实盘波动可能瞬间扫掉止损。 核心计算塞进一个全静态类,单函数里数出看涨/看跌柱并归并信号分量;主体函数按信号强度和百分比质量开仓,只要信号轻微反转就平仓。柱不参与历史远端计算,越远权重越低,这是市场物理属性的粗略近似,下一篇再展开。 下面这段代码是EA头部的库引用、枚举、输入参数与数组预备函数,逐行拆完你就能在MT5里建文件跑起来:
class="macro">#include <Trade\PositionInfo.mqh> class="macro">#include <Trade\Trade.mqh> CPositionInfo m_position=CPositionInfo();class=class="str">"cmt">// trade position object CTrade m_trade=CTrade(); class=class="str">"cmt">// trading object enum MODE_CALCULATE { MODE_1=class="num">0, MODE_2=class="num">1, MODE_3=class="num">2, MODE_4=class="num">3 }; class="kw">input MODE_CALCULATE MODEE=MODE_1;class=class="str">"cmt">//Mode class="kw">input class="type">int TradeHour=class="num">0;class=class="str">"cmt">//Start Trading Hour class="kw">input class="type">int TradeMinute=class="num">1;class=class="str">"cmt">//Start Trading Minute class="kw">input class="type">int TradeHourEnd=class="num">23;class=class="str">"cmt">//End Trading Hour class="kw">input class="type">int TradeMinuteEnd=class="num">59;class=class="str">"cmt">//End Trading Minute class="kw">input class="type">bool bWriteValuesE=class="kw">false;class=class="str">"cmt">//Log class="kw">input class="type">int CandlesE=class="num">50;class=class="str">"cmt">//Bars To Analyse class="kw">input class="type">int Signal=class="num">200;class=class="str">"cmt">//Signal Power class="kw">input class="type">int PercentE=class="num">52;class=class="str">"cmt">//Percent Signals To One Side class="kw">input class="type">bool bInvert=class="kw">false;class=class="str">"cmt">//Trade Invert class="kw">input class="type">int SLE=class="num">3000;class=class="str">"cmt">//Stop Loss Points class="kw">input class="type">int TPE=class="num">3000;class=class="str">"cmt">//Take Profit Points class="kw">input class="type">class="kw">double Lot=class="num">0.01;class=class="str">"cmt">//Lot class="kw">input class="type">int MagicF=class="num">15670867;class=class="str">"cmt">//Magic class="type">MqlTick LastTick;class=class="str">"cmt">//the last tick class="type">class="kw">double High[]; class="type">class="kw">double Low[]; class="type">class="kw">double Close[]; class="type">class="kw">double Open[]; class="type">class="kw">datetime Time[]; class="type">long Volume[]; class="type">void DimensionAllMQL5Values()class=class="str">"cmt">//prepare the arrays { ArrayResize(Time,CandlesE,class="num">0); ArrayResize(High,CandlesE,class="num">0); ArrayResize(Close,CandlesE,class="num">0); ArrayResize(Open,CandlesE,class="num">0); ArrayResize(Low,CandlesE,class="num">0); ArrayResize(Volume,CandlesE,class="num">0); } class="type">void CalcAllMQL5Values()class=class="str">"cmt">//recalculate the arrays {
◍ 先把数组方向掰正再灌数据
在 MT5 里用 CopyHigh / CopyLow 这类函数往自定义数组塞历史行情时,数组默认索引方向会直接影响后面循环里的 i=0 到底指向最新 K 线还是最老 K 线。上面这段做法很直接:先统一 ArraySetAsSeries(...,false) 把 High、Low、Close、Open、Time、Volume 全部改成正序(i=0 为最早一根),再执行 Copy 系列把最近 CandlesE 根行情填进去,最后又全部 ArraySetAsSeries(...,true) 翻回倒序(i=0 为最新一根)。 这么绕一圈的意义在于:Copy 函数本身不挑数组方向,但 TickBox::CalculateAll 里的 for(i=0;i<CandlesE;i++) 配合 Open[i]<Close[i] 统计阳线根数时,若数组已是倒序,i 的遍历顺序就是自新向旧,逻辑可读性更高。你可以在 EA 的 OnCalculate 开头照抄这 12 行 SetAsSeries + Copy,把 CandlesE 设成 500 跑一遍,观察数组[0] 的 Time[0] 是否等于当前柱时间。 TickBox 这个类把多空统计做成静态成员:BarsUp、BarsDown 计阳线阴线数,PowerUp / PowerDown 累实体幅度,Percent 系列留作后续算占比。CalculateAll 接收 MODE_CALCULATE 参数,当 MODE0==MODE_1 时清零后遍历 CandlesE 根,Open[i]<Close[i] 就 BarsUp++。外汇与贵金属波动受杠杆影响,统计结果仅反映样本内概率倾向,实盘前务必在策略测试器用真实点差校验。
ArraySetAsSeries(High,class="kw">false); ArraySetAsSeries(Low,class="kw">false); ArraySetAsSeries(Close,class="kw">false); ArraySetAsSeries(Open,class="kw">false); ArraySetAsSeries(Time,class="kw">false); ArraySetAsSeries(Volume,class="kw">false); CopyHigh(_Symbol,_Period,class="num">0,CandlesE,High); CopyLow(_Symbol,_Period,class="num">0,CandlesE,Low); CopyClose(_Symbol,_Period,class="num">0,CandlesE,Close); CopyOpen(_Symbol,_Period,class="num">0,CandlesE,Open); CopyTime(_Symbol,_Period,class="num">0,CandlesE,Time); CopyTickVolume(_Symbol,_Period,class="num">0,CandlesE,Volume); ArraySetAsSeries(High,true); ArraySetAsSeries(Low,true); ArraySetAsSeries(Close,true); ArraySetAsSeries(Open,true); ArraySetAsSeries(Time,true); ArraySetAsSeries(Volume,true); } class TickBox { class="kw">public: class="kw">static class="type">int BarsUp; class="kw">static class="type">int BarsDown; class="kw">static class="type">class="kw">double PowerUp; class="kw">static class="type">class="kw">double PowerDown; class="kw">static class="type">class="kw">double PercentUp; class="kw">static class="type">class="kw">double PercentDown; class="kw">static class="type">class="kw">double PercentPowerUp; class="kw">static class="type">class="kw">double PercentPowerDown; class="kw">static class="type">void CalculateAll(MODE_CALCULATE MODE0)class=class="str">"cmt">//calculate all the necessary parameters { BarsUp=class="num">0; BarsDown=class="num">0; PercentUp=class="num">0.0; PercentDown=class="num">0.0; PowerUp=class="num">0.0; PowerDown=class="num">0.0; if ( MODE0 == MODE_1 ) { for ( class="type">int i=class="num">0; i<CandlesE; i++ ) { if ( Open[i] < Close[i] ) { BarsUp++;
「多模式力度累加的写法差异」
这段逻辑在遍历最近 CandlesE 根 K 线,按收阳或收阴分别累加 BarsUp / BarsDown 计数,以及 PowerUp / PowerDown 的加权力度。核心区别在于 MODE0 的取值决定分母与权重系数,直接影响力度数值的量纲。 MODE_2 用 _Point 作分母,把实体绝对值换算成点数再乘成交量;其中阴线分支写了除以 -_Point,会得到负值,若后续取绝对值需留意符号。MODE_3 在 MODE_2 基础上乘了 (CandlesE-i)/CandlesE 的线性衰减权重,越靠近当前的 K 线权重越接近 1,远端趋近 0。 MODE_4 则把 MODE_3 的 _Point 分母换成 (High[i]-Low[i]) 即振幅,相当于用实体占振幅比来标准化,再乘衰减权重与成交量。外汇与贵金属波动跳空频繁,High-Low 可能为极小值,实盘前应在 MT5 用 Print 打出几组 PowerUp 数值确认无异常溢出。
PowerUp+=(MathAbs(Open[i] - Close[i])/(High[i] - Low[i]))*Volume[i]; } if ( Open[i] > Close[i] ) { BarsDown++; PowerDown+=(MathAbs(Open[i] - Close[i])/(High[i] - Low[i]))*Volume[i]; } } if ( MODE0 == MODE_2 ) { for ( class="type">int i=class="num">0; i<CandlesE; i++ ) { if ( Open[i] < Close[i] ) { BarsUp++; PowerUp+=(MathAbs(Open[i] - Close[i])/_Point)*Volume[i]; } if ( Open[i] > Close[i] ) { BarsDown++; PowerDown+=(MathAbs(Open[i] - Close[i])/-_Point)*Volume[i]; } } } if ( MODE0 == MODE_3 ) { for ( class="type">int i=class="num">0; i<CandlesE; i++ ) { if ( Open[i] < Close[i] ) { BarsUp++; PowerUp+=(class="type">class="kw">double(CandlesE-i)/class="type">class="kw">double(CandlesE))*(MathAbs(Open[i] - Close[i])/_Point)*Volume[i]; } if ( Open[i] > Close[i] ) { BarsDown++; PowerDown+=(class="type">class="kw">double(CandlesE-i)/class="type">class="kw">double(CandlesE))*(MathAbs(Open[i] - Close[i])/_Point)*Volume[i]; } } } if ( MODE0 == MODE_4 ) { for ( class="type">int i=class="num">0; i<CandlesE; i++ ) { if ( Open[i] < Close[i] ) { BarsUp++; PowerUp+=(class="type">class="kw">double(CandlesE-i)/class="type">class="kw">double(CandlesE))*(MathAbs(Open[i] - Close[i])/(High[i] - Low[i]))*Volume[i]; } if ( Open[i] > Close[i] ) {
多空统计落地到平仓触发
上面那段累计逻辑跑完以后,类静态成员里就存了 BarsUp、BarsDown 以及带成交量加权的 PowerUp、PowerDown。接着用两组百分比把「数量差」和「力度差」分开表达:PercentUp 是上涨 bar 数占比,PercentPowerUp 是上涨力度占总力度的比例,两者分母不同,前者看次数、后者看加权能量。 Trade() 函数先取当前 tick 和时间结构,把小时分钟折成 MinuteEquivalent(例如 14:30 就是 870),再和交易时段边界做比对。当上下 bar 数差绝对值 ≥ 1.0 且 PercentPowerUp ≥ 50.0 时,倾向认为多头力度占优,此时若未反转参数就平掉买仓,反转则平卖仓;对称条件下平另一方向。 时段判断里有个易踩的坑:若起始分钟大于结束分钟(如 22:00 到次日 02:00),代码用「不落在结束~起始区间内」来放行交易。此时若仓数为 0、数量差 ≥ 外部 Signal 参数、且 PercentPowerUp ≥ PercentE,就按 bInvert 决定卖还是买,并挂上以 _Point 为单位的 SL/TP。外汇与贵金属波动剧烈,这套触发在高滑点时段可能偏离回测表现,上 MT5 用策略测试器跑不同 Signal 值验证最稳妥。
BarsDown++;
PowerDown+=(class="type">class="kw">double(CandlesE-i)/class="type">class="kw">double(CandlesE))*(MathAbs(Open[i] - Close[i])/(High[i] - Low[i]))*Volume[i];
}
}
}
if ( BarsUp > class="num">0 && BarsDown > class="num">0 )
{
PercentUp=(class="type">class="kw">double(BarsUp)/class="type">class="kw">double(BarsUp+BarsDown))*class="num">100.0;
PercentDown=(class="type">class="kw">double(BarsDown)/class="type">class="kw">double(BarsUp+BarsDown))*class="num">100.0;
PercentPowerUp=(class="type">class="kw">double(PowerUp)/class="type">class="kw">double(PowerUp+PowerDown))*class="num">100.0;
PercentPowerDown=(class="type">class="kw">double(PowerDown)/class="type">class="kw">double(PowerUp+PowerDown))*class="num">100.0;
}
};
class="type">int TickBox::BarsUp=class="num">0;
class="type">int TickBox::BarsDown=class="num">0;
class="type">class="kw">double TickBox::PowerUp=class="num">0;
class="type">class="kw">double TickBox::PowerDown=class="num">0;
class="type">class="kw">double TickBox::PercentUp=class="num">0;
class="type">class="kw">double TickBox::PercentDown=class="num">0;
class="type">class="kw">double TickBox::PercentPowerUp=class="num">0;
class="type">class="kw">double TickBox::PercentPowerDown=class="num">0;
class="type">void Trade()
{
SymbolInfoTick(Symbol(),LastTick);
class="type">MqlDateTime tm;
TimeToStruct(LastTick.time,tm);
class="type">int MinuteEquivalent=tm.hour*class="num">60+tm.min;
class="type">int BorderMinuteStartTrade=HourCorrect(TradeHour)*class="num">60+MinuteCorrect(TradeMinute);
class="type">int BorderMinuteEndTrade=HourCorrect(TradeHourEnd)*class="num">60+MinuteCorrect(TradeMinuteEnd);
if ( MathAbs(TickBox::BarsUp-TickBox::BarsDown) >= class="num">1.0 && TickBox::PercentPowerUp >= class="num">50.0 )
{
if ( !bInvert ) ClosePosition(POSITION_TYPE_BUY);
else ClosePosition(POSITION_TYPE_SELL);
}
if ( MathAbs(TickBox::BarsUp-TickBox::BarsDown) >= class="num">1.0 && TickBox::PercentPowerDown >= class="num">50.0 )
{
if ( !bInvert ) ClosePosition(POSITION_TYPE_SELL);
else ClosePosition(POSITION_TYPE_BUY);
}
if ( BorderMinuteStartTrade > BorderMinuteEndTrade )
{
if ( PositionsTotal() == class="num">0 && !(MinuteEquivalent>=BorderMinuteEndTrade && MinuteEquivalent<= BorderMinuteStartTrade) )
{
if ( MathAbs(TickBox::BarsUp-TickBox::BarsDown) >= Signal && TickBox::PercentPowerUp >= PercentE )
{
if ( !bInvert ) m_trade.Sell(Lot,_Symbol,LastTick.ask,LastTick.ask+class="type">class="kw">double(SLE)*_Point,LastTick.bid-class="type">class="kw">double(TPE)*_Point);◍ 交易触发与平仓的双向分支
这段逻辑把开仓拆成了两段对称结构:先判断当前无持仓且分钟数落在 BorderMinuteStartTrade 到 BorderMinuteEndTrade 区间内,再比对 TickBox 里记录的上下行 bars 差与能量百分比。 当 BarsUp 与 BarsDown 差值的绝对值不小于 Signal,且 PercentPowerUp 达到 PercentE 时,默认方向走 Sell,反转标志 bInvert 为真则改为 Buy;下行动能达标时方向相反。止损用 SLE*_Point 偏移挂出,止盈用 TPE*_Point,外汇与贵金属杠杆高,实盘前务必把 SLE、TPE 按品种波动性重算。 ClosePosition 只处理本 EA 的 MagicF 标签仓位,且要求传入方向与实际持仓类型一致才调用 PositionClose,避免误平其他策略单。 下面这段是原文开仓与平仓调用的核心片段,可在 MT5 里直接对照函数签名验证:
else m_trade.Buy(Lot,_Symbol,LastTick.ask,LastTick.bid-class="type">class="kw">double(SLE)*_Point,LastTick.ask+class="type">class="kw">double(TPE)*_Point); } if ( MathAbs(TickBox::BarsUp-TickBox::BarsDown) >= Signal && TickBox::PercentPowerDown >= PercentE ) { if ( !bInvert ) m_trade.Buy(Lot,_Symbol,LastTick.ask,LastTick.bid-class="type">class="kw">double(SLE)*_Point,LastTick.ask+class="type">class="kw">double(TPE)*_Point); else m_trade.Sell(Lot,_Symbol,LastTick.ask,LastTick.ask+class="type">class="kw">double(SLE)*_Point,LastTick.bid-class="type">class="kw">double(TPE)*_Point); } } if ( PositionsTotal() == class="num">0 && BorderMinuteStartTrade <= BorderMinuteEndTrade ) { if ( MinuteEquivalent>=BorderMinuteStartTrade && MinuteEquivalent<= BorderMinuteEndTrade ) { if ( MathAbs(TickBox::BarsUp-TickBox::BarsDown) >= Signal && TickBox::PercentPowerUp >= PercentE ) { if ( !bInvert ) m_trade.Sell(Lot,_Symbol,LastTick.ask,LastTick.ask+class="type">class="kw">double(SLE)*_Point,LastTick.bid-class="type">class="kw">double(TPE)*_Point); else m_trade.Buy(Lot,_Symbol,LastTick.ask,LastTick.bid-class="type">class="kw">double(SLE)*_Point,LastTick.ask+class="type">class="kw">double(TPE)*_Point); } if ( MathAbs(TickBox::BarsUp-TickBox::BarsDown) >= Signal && TickBox::PercentPowerDown >= PercentE ) { if ( !bInvert ) m_trade.Buy(Lot,_Symbol,LastTick.ask,LastTick.bid-class="type">class="kw">double(SLE)*_Point,LastTick.ask+class="type">class="kw">double(TPE)*_Point); else m_trade.Sell(Lot,_Symbol,LastTick.ask,LastTick.ask+class="type">class="kw">double(SLE)*_Point,LastTick.bid-class="type">class="kw">double(TPE)*_Point); } } } } class="type">void ClosePosition(class="type">ENUM_POSITION_TYPE Direction)class=class="str">"cmt">//close a position by a symbol { class="type">bool ord; ord=PositionSelect(Symbol()); if ( ord && class="type">int(PositionGetInteger(POSITION_MAGIC)) == MagicF && Direction == class="type">ENUM_POSITION_TYPE(PositionGetInteger(POSITION_TYPE)) ) { if(m_position.SelectByIndex(class="num">0)) m_trade.PositionClose(m_position.Ticket()); } } class="type">int OnInit() {
「用 K 线闭合触发计算而非逐 tick 空转」
EA 在 OnInit 里先给持仓打上魔术号 m_trade.SetExpertMagicNumber(MagicF),再调用 DimensionAllMQL5Values() 把预定义数组维度化,最后返回 INIT_SUCCEEDED。这一步没做扎实,后续 OnTick 里取到的指标数组可能是空的。 真正的节奏控制藏在 bNewBar() 里:每 tick 调 CopyTime 取当前品种周期最新 1 根 K 线时间到 TimeX[0],跟模块级变量 Time0 比。Time0 初值为 0,首根 K 线只记录时间、返回 false;之后只要 Time0 小于新时间,就刷新 Time0 并返回 true,代表一根新柱走完。 OnTick 只在 bNewBar() 返回 true 时才干活:重算指标、跑 TickBox::CalculateAll(MODEE),若 bWriteValuesE 开关打开,就把多空占比、信号强度、阻支撑强度打印到日志。外汇与贵金属波动剧烈,这种按柱触发的方式能省掉大量无谓计算,但信号滞后一根柱,实盘前请在 MT5 策略测试器里用不同周期验证打印数值是否符合预期。
m_trade.SetExpertMagicNumber(MagicF);class=class="str">"cmt">//set the magic number for positions DimensionAllMQL5Values();class=class="str">"cmt">//prepare the predefined arrays class="kw">return(INIT_SUCCEEDED); } class="type">class="kw">datetime Time0; class="type">class="kw">datetime TimeX[class="num">1]; class="type">bool bNewBar() { CopyTime(_Symbol,_Period,class="num">0,class="num">1,TimeX); if ( Time0 < TimeX[class="num">0] ) { if (Time0 != class="num">0) { Time0=TimeX[class="num">0]; class="kw">return true; } else { Time0=TimeX[class="num">0]; class="kw">return class="kw">false; } } else class="kw">return class="kw">false; } class="type">void OnTick() { if ( bNewBar())class=class="str">"cmt">//work by bars { CalcAllMQL5Values(); TickBox::CalculateAll(MODEE); if (bWriteValuesE) { Print("% Sit in buy = ",TickBox::PercentUp); Print("% Sit in sell = ",TickBox::PercentDown); Print("Signal = ",MathAbs(TickBox::BarsDown-TickBox::BarsUp)); Print("% Resistance = ",TickBox::PercentPowerUp); Print("% Support = ",TickBox::PercentPowerDown); Print("***************************************************************************"); } Trade(); } }
EURUSD M5 上的十年回测观察
拿 EA 在 EURUSD M5 跑,先从去年一段开始验,再拉长到 10 年区间交替换操作模式与服务器时间间隔。按这套交替办法,大约在 30–60 分钟窗口里能抓出一组可接受的参数。 去年切片图形干净,信号延续性明显;抽 10 年里的第一年回测,图不如近期漂亮,但净值变化方向还在,说明底层逻辑没断。取中间年份段测,也能看到全局模式痕迹,拉满整段区间后虽不完美,但存在有效阶段,可考虑加过滤器或深优化。 外汇与贵金属属高风险品种,多货币能力要靠持续改代码实现,本文不展开。当前形态交易次数少但跨品种有效,最简代码也能当想法底座。若死磕系统却挤不出结果,大概率该换思路而非硬修。 柱形图参数对各货币对影响不同,极端值组合因对而异;也有全对通用的极端值,但得靠时间与实践捞。读者可自开 MT5 用去年切片先复一遍再扩区间。
◍ 记住这一条就够了
写系统最忌堆复杂度。参数一多,自由度就膨胀,回测时间也跟着拉长,EURUSD M5 上挂满数千个指标的图表我见过太多,最后没人能从中看出稳定结构。 简单系统反而更经得起验证:少几个输入变量,过拟合空间就被砍掉一大块。别信「30 笔成交就稳了」这种说法,样本量太小说明不了任何事。 动手前先定一个可接受的时间表和目标,做不到再缩范围,也比盲写强。外汇和贵金属杠杆高,陷进去容易脱身难,把它当爱好还是当盈利工具,得先想清楚。