MQL5自动化交易策略(第二十六部分):构建针形K线均价加仓的多持仓交易系统
引言
在前一篇文章(第二十五部分)中,我们使用MetaQuotes Language 5(MQL5)开发了一套趋势线交易系统。该系统通过最小二乘拟合识别支撑、阻力趋势线,依据价格触碰趋势线触发自动交易,并提供可视化反馈。在第二十六部分中,我们将开发针形K线均价加仓程序:识别针形K线形态作为开仓信号,依靠均价加仓策略管理多笔持仓;同时集成移动止损、盈亏平衡调整功能,以及实时监控的信息面板。我们将涵盖以下主题:
阅读完本文后,您将得到一套功能强大、基于针形K线的MQL5交易策略,可直接进行自定义修改 —— 让我们开始吧!
解析针形K线均价加仓系统框架
我们正在搭建一套自动化交易系统,利用针形K线形态进行交易。该形态由单根K线构成,特点是影线较长、实体很小,往往预示着价格在关键价位发生强势反转。针形K线策略在交易中很常见,它能够识别出价格被拒绝的市场节点,提供高概率的交易入场信号,配合支撑位与阻力位效果更优。下方为部分常见形态的示意图。

我们的方法将重点放在当前时间周期下识别针形K线,当行情朝着与初始开仓相反方向运行时,采用均价加仓策略建立额外的持仓。该策略旨在提升整体交易结果,同时借助移动止损与盈亏平衡调整来管控风险。为此,我们首先以前一根H4 K线的收盘价作为支撑或阻力参考位,并据此识别针形K线,从而使交易信号与关键市场区域保持一致。
接着实现均价加仓机制,按照预设价格间隔追加仓位,提升策略在行情波动环境下的适应性。最后接入信息面板,展示实时交易指标,并通过线条等可视化标记关键价位,实现对策略的有效监控与参数调整。先了解我们要实现的整体目标,之后就可以进入代码实现环节。

在MQL5中的实现
要在MQL5中创建该程序,请打开MetaEditor,在导航器中找到“指标”文件夹,点击“新建”选项卡,并按照向导提示创建文件。完成上述准备后,在代码编辑环境中,我们首先声明部分输入参数与全局变量,以此提升程序的动态适配能力。
//+------------------------------------------------------------------+ //| a. Pin Bar Averaging EA.mq5 | //| Copyright 2025, Allan Munene Mutiiria. | //| https://t.me/Forex_Algo_Trader | //+------------------------------------------------------------------+ #property copyright "Copyright 2025, Allan Munene Mutiiria." #property link "https://t.me/Forex_Algo_Trader" #property version "1.00" #property strict #include <Trade\Trade.mqh> //--- Include Trade library for trading operations CTrade obj_Trade; //--- Instantiate trade object //+------------------------------------------------------------------+ //| Trading signal enumeration | //+------------------------------------------------------------------+ enum EnableTradingBySignal { //--- Define trading signal enum ENABLED = 1, // Enable trading signals DISABLED = 0 // Disable trading signals }; //+------------------------------------------------------------------+ //| Input parameters | //+------------------------------------------------------------------+ input bool useSignalMode = DISABLED; // Set signal mode (ENABLED/DISABLED) input int orderDistancePips = 50; // Set order distance (pips) input double lotMultiplier = 1; // Set lot size multiplier input bool useRSIFilter = false; // Enable RSI filter input int magicNumber = 123456789; // Set magic number input double initialLotSize = 0.01; // Set initial lot size input int compoundPercent = 2; // Set compounding percent (0 for fixed lots) input int maxOrders = 5; // Set maximum orders input double stopLossPips = 400; // Set stop loss (pips) input double takeProfitPips = 200; // Set take profit (pips) input bool useAutoTakeProfit = true; // Enable auto take profit input bool useTrailingStop = true; // Enable trailing stop input double trailingStartPips = 15; // Set trailing start (pips) input double breakevenPips = 10; // Set breakeven (pips) input string orderComment = "Forex_Algo_Trader"; // Set order comment input color lineColor = clrBlue; // Set line color input int lineWidth = 2; // Set line width //+------------------------------------------------------------------+ //| Global variables | //+------------------------------------------------------------------+ bool isTradingAllowed(); //--- Declare trading allowed check double slBreakevenMinus = 0; //--- Initialize breakeven minus double normalizedPoint; //--- Declare normalized point ulong currentTicket = 0; //--- Initialize current ticket double buyCount, currentBuyLot, totalBuyLots; //--- Declare buy metrics double sellCount, currentSellLot, totalSellLots; //--- Declare sell metrics double totalSum, totalSwap; //--- Declare total sum and swap double buyProfit, sellProfit, totalOperations; //--- Declare profit and operations double buyWeightedSum, sellWeightedSum; //--- Declare weighted sums double buyBreakEvenPrice, sellBreakEvenPrice; //--- Declare breakeven prices double minBuyLot, minSellLot; //--- Declare minimum lot sizes double maxSellPrice, minBuyPrice; //--- Declare price extremes
为了搭建MQL5平台下的针形K线均价加仓交易系统 ,实现基于针形K线形态的自动化交易以及持仓管理体系,我们首先引入"<Trade\Trade.mqh>"库,并实例化obj_Trade作为CTrade对象,用于处理开仓、平仓等各类交易操作。接下来,我们定义枚举类型"EnableTradingBySignal",包含"ENABLED"(1)与"DISABLED"(0)两个枚举值,用来控制是否依据交易信号执行持仓管理。随后,我们设置可自定义EA的输入参数:布尔类型开关用于切换信号模式、以点为单位的加仓间距、手数乘数、RSI指标过滤器开关、用于区分本EA订单的magic数字、初始交易手数、复利百分比(设置为0代表固定手数)、最大订单数量、以点为单位的止损与止盈、自动止盈开关、移动止损开关、移动止损启动点位、盈亏平衡触发点位(单位点)、订单注释文本,以及可视化标记线条的颜色和线宽。
最后,我们声明全局变量:用于校验交易执行条件的函数"isTradingAllowed";供止损调整使用的"slBreakevenMinus"(初始值为0);用于价格换算的"normalizedPoint";用于跟踪订单的"currentTicket";以及一系列计数器与汇总变量,例如"buyCount"、"currentBuyLot"、"totalBuyLots"、"sellCount"、"currentSellLot"、"totalSellLots"、"totalSum"、"totalSwap"、"buyProfit"、"sellProfit"、"totalOperations"、"buyWeightedSum"、"sellWeightedSum"、"buyBreakEvenPrice"、"sellBreakEvenPrice"、"minBuyLot"、"minSellLot"、"maxSellPrice"和"minBuyPrice"。至此完成EA的核心框架搭建,可支持针形K线识别与均价加仓逻辑。主要逻辑将在基于Tick的执行过程中完成,之后我们就可以执行程序初始化。
//+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { normalizedPoint = _Point; //--- Initialize point value if (_Digits == 5 || _Digits == 3) { //--- Check for 5 or 3 digit symbols normalizedPoint *= 10; //--- Adjust point value } ChartSetInteger(0, CHART_SHOW_GRID, false); //--- Disable chart grid obj_Trade.SetExpertMagicNumber(magicNumber); //--- Set magic number for trade object obj_Trade.SetTypeFilling(ORDER_FILLING_IOC); //--- Set order filling type return(INIT_SUCCEEDED); //--- Return success } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { ObjectsDeleteAll(0); //--- Delete all chart objects ChartRedraw(0); //--- Redraw chart }
首先,在OnInit事件处理器中:将"normalizedPoint"初始化为内置变量_Point;针对3位或5位报价的品种,结合_Digits将其乘以10做修正,保障价格计算精准;调用ChartSetInteger,将CHART_SHOW_GRID设置为false,关闭图表网格,让界面更加整洁;通过"SetExpertMagicNumber"将"magicNumber"配置给交易对象"obj_Trade",用于识别本EA发起的交易;调用"SetTypeFilling"将订单成交模式设置为"ORDER_FILLING_IOC";最后返回"INIT_SUCCEEDED",代表初始化完成。接下来轮到OnDeinit事件处理器:调用ObjectsDeleteAll删除图表上全部对象,清理后续会创建的仪表盘、标记线条等可视化元素。执行该操作是为了确保图表资源得到彻底释放,再调用ChartRedraw刷新图表,确保程序干净退出。在深入复杂的交易逻辑之前,我们先定义若干辅助函数,用于让程序具备更好的动态特性,同时便于后期维护。
//+------------------------------------------------------------------+ //| Count total trades | //+------------------------------------------------------------------+ int CountTrades() { int positionCount = 0; //--- Initialize position count for (int trade = PositionsTotal() - 1; trade >= 0; trade--) { //--- Iterate through positions ulong ticket = PositionGetTicket(trade); //--- Get position ticket if (ticket == 0) continue; //--- Skip invalid tickets if (PositionGetString(POSITION_SYMBOL) != Symbol() || PositionGetInteger(POSITION_MAGIC) != magicNumber) continue; //--- Skip non-matching positions if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL || PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) { //--- Check trade type positionCount++; //--- Increment position count } } return(positionCount); //--- Return total count } //+------------------------------------------------------------------+ //| Count buy trades | //+------------------------------------------------------------------+ int CountTradesBuy() { int buyPositionCount = 0; //--- Initialize buy position count for (int trade = PositionsTotal() - 1; trade >= 0; trade--) { //--- Iterate through positions ulong ticket = PositionGetTicket(trade); //--- Get position ticket if (ticket == 0) continue; //--- Skip invalid tickets if (PositionGetString(POSITION_SYMBOL) != Symbol() || PositionGetInteger(POSITION_MAGIC) != magicNumber) continue; //--- Skip non-matching positions if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) { //--- Check buy position buyPositionCount++; //--- Increment buy count } } return(buyPositionCount); //--- Return buy count } //+------------------------------------------------------------------+ //| Count sell trades | //+------------------------------------------------------------------+ int CountTradesSell() { int sellPositionCount = 0; //--- Initialize sell position count for (int trade = PositionsTotal() - 1; trade >= 0; trade--) { //--- Iterate through positions ulong ticket = PositionGetTicket(trade); //--- Get position ticket if (ticket == 0) continue; //--- Skip invalid tickets if (PositionGetString(POSITION_SYMBOL) != Symbol() || PositionGetInteger(POSITION_MAGIC) != magicNumber) continue; //--- Skip non-matching positions if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL) { //--- Check sell position sellPositionCount++; //--- Increment sell count } } return(sellPositionCount); //--- Return sell count } //+------------------------------------------------------------------+ //| Normalize price | //+------------------------------------------------------------------+ double NormalizePrice(double price) { return(NormalizeDouble(price, _Digits)); //--- Normalize price to symbol digits } //+------------------------------------------------------------------+ //| Get lot digit for normalization | //+------------------------------------------------------------------+ int fnGetLotDigit() { double lotStepValue = SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_STEP); //--- Get lot step value if (lotStepValue == 1) return(0); //--- Return 0 for step 1 if (lotStepValue == 0.1) return(1); //--- Return 1 for step 0.1 if (lotStepValue == 0.01) return(2); //--- Return 2 for step 0.01 if (lotStepValue == 0.001) return(3); //--- Return 3 for step 0.001 if (lotStepValue == 0.0001) return(4); //--- Return 4 for step 0.0001 return(1); //--- Default to 1 } //+------------------------------------------------------------------+ //| Check buy orders for specific magic number | //+------------------------------------------------------------------+ int CheckBuyOrders(int magic) { int buyOrderCount = 0; //--- Initialize buy order count for (int i = PositionsTotal() - 1; i >= 0; i--) { //--- Iterate through positions ulong ticket = PositionGetTicket(i); //--- Get position ticket if (ticket == 0) continue; //--- Skip invalid tickets if (PositionGetInteger(POSITION_MAGIC) != magic) continue; //--- Skip non-matching magic if (PositionGetString(POSITION_SYMBOL) == Symbol()) { //--- Check symbol if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) { //--- Check buy position buyOrderCount++; //--- Increment buy count break; //--- Exit loop } } } return(buyOrderCount); //--- Return buy order count } //+------------------------------------------------------------------+ //| Check sell orders for specific magic number | //+------------------------------------------------------------------+ int CheckSellOrders(int magic) { int sellOrderCount = 0; //--- Initialize sell order count for (int i = PositionsTotal() - 1; i >= 0; i--) { //--- Iterate through positions ulong ticket = PositionGetTicket(i); //--- Get position ticket if (ticket == 0) continue; //--- Skip invalid tickets if (PositionGetInteger(POSITION_MAGIC) != magic) continue; //--- Skip non-matching magic if (PositionGetString(POSITION_SYMBOL) == Symbol()) { //--- Check symbol if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL) { //--- Check sell position sellOrderCount++; //--- Increment sell count break; //--- Exit loop } } } return(sellOrderCount); //--- Return sell order count } //+------------------------------------------------------------------+ //| Check total buy orders | //+------------------------------------------------------------------+ int CheckTotalBuyOrders(int magic) { int totalBuyOrderCount = 0; //--- Initialize total buy order count for (int i = PositionsTotal() - 1; i >= 0; i--) { //--- Iterate through positions ulong ticket = PositionGetTicket(i); //--- Get position ticket if (ticket == 0) continue; //--- Skip invalid tickets if (PositionGetInteger(POSITION_MAGIC) != magic) continue; //--- Skip non-matching magic if (PositionGetString(POSITION_SYMBOL) == Symbol()) { //--- Check symbol if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) { //--- Check buy position totalBuyOrderCount++; //--- Increment buy count } } } return(totalBuyOrderCount); //--- Return total buy count } //+------------------------------------------------------------------+ //| Check total sell orders | //+------------------------------------------------------------------+ int CheckTotalSellOrders(int magic) { int totalSellOrderCount = 0; //--- Initialize total sell order count for (int i = PositionsTotal() - 1; i >= 0; i--) { //--- Iterate through positions ulong ticket = PositionGetTicket(i); //--- Get position ticket if (ticket == 0) continue; //--- Skip invalid tickets if (PositionGetInteger(POSITION_MAGIC) != magic) continue; //--- Skip non-matching magic if (PositionGetString(POSITION_SYMBOL) == Symbol()) { //--- Check symbol if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL) { //--- Check sell position totalSellOrderCount++; //--- Increment sell count } } } return(totalSellOrderCount); //--- Return total sell count } //+------------------------------------------------------------------+ //| Check market buy orders | //+------------------------------------------------------------------+ int CheckMarketBuyOrders() { int marketBuyCount = 0; //--- Initialize market buy count for (int i = PositionsTotal() - 1; i >= 0; i--) { //--- Iterate through positions ulong ticket = PositionGetTicket(i); //--- Get position ticket if (ticket == 0) continue; //--- Skip invalid tickets if (PositionGetInteger(POSITION_MAGIC) != magicNumber) continue; //--- Skip non-matching magic if (PositionGetString(POSITION_SYMBOL) == Symbol()) { //--- Check symbol if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) { //--- Check buy position marketBuyCount++; //--- Increment buy count } } } return(marketBuyCount); //--- Return market buy count } //+------------------------------------------------------------------+ //| Check market sell orders | //+------------------------------------------------------------------+ int CheckMarketSellOrders() { int marketSellCount = 0; //--- Initialize market sell count for (int i = PositionsTotal() - 1; i >= 0; i--) { //--- Iterate through positions ulong ticket = PositionGetTicket(i); //--- Get position ticket if (ticket == 0) continue; //--- Skip invalid tickets if (PositionGetInteger(POSITION_MAGIC) != magicNumber) continue; //--- Skip non-matching magic if (PositionGetString(POSITION_SYMBOL) == Symbol()) { //--- Check symbol if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL) { //--- Check sell position marketSellCount++; //--- Increment sell count } } } return(marketSellCount); //--- Return market sell count } //+------------------------------------------------------------------+ //| Close all buy positions | //+------------------------------------------------------------------+ void CloseBuy() { while (CheckMarketBuyOrders() > 0) { //--- Check buy orders exist for (int i = PositionsTotal() - 1; i >= 0; i--) { //--- Iterate through positions ulong ticket = PositionGetTicket(i); //--- Get position ticket if (ticket == 0) continue; //--- Skip invalid tickets if (PositionGetString(POSITION_SYMBOL) == Symbol() && PositionGetInteger(POSITION_MAGIC) == magicNumber) { //--- Check symbol and magic if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) { //--- Check buy position obj_Trade.PositionClose(ticket); //--- Close position } } } } } //+------------------------------------------------------------------+ //| Close all sell positions | //+------------------------------------------------------------------+ void CloseSell() { while (CheckMarketSellOrders() > 0) { //--- Check sell orders exist for (int i = PositionsTotal() - 1; i >= 0; i--) { //--- Iterate through positions ulong ticket = PositionGetTicket(i); //--- Get position ticket if (ticket == 0) continue; //--- Skip invalid tickets if (PositionGetString(POSITION_SYMBOL) == Symbol() && PositionGetInteger(POSITION_MAGIC) == magicNumber) { //--- Check symbol and magic if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL) { //--- Check sell position obj_Trade.PositionClose(ticket); //--- Close position } } } } } //+------------------------------------------------------------------+ //| Calculate lot size | //+------------------------------------------------------------------+ double GetLots() { double calculatedLot; //--- Initialize calculated lot double minLot = SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_MIN); //--- Get minimum lot double maxLot = SymbolInfoDouble(Symbol(), SYMBOL_VOLUME_MAX); //--- Get maximum lot if (compoundPercent != 0) { //--- Check compounding calculatedLot = NormalizeDouble(AccountInfoDouble(ACCOUNT_BALANCE) * compoundPercent / 100 / 10000, fnGetLotDigit()); //--- Calculate compounded lot if (calculatedLot < minLot) calculatedLot = minLot; //--- Enforce minimum lot if (calculatedLot > maxLot) calculatedLot = maxLot; //--- Enforce maximum lot } else { calculatedLot = initialLotSize; //--- Use fixed lot size } return(calculatedLot); //--- Return calculated lot } //+------------------------------------------------------------------+ //| Check account free margin | //+------------------------------------------------------------------+ double AccountFreeMarginCheck(string symbol, int orderType, double volume) { double marginRequired = 0.0; //--- Initialize margin required double price = orderType == ORDER_TYPE_BUY ? SymbolInfoDouble(symbol, SYMBOL_ASK) : SymbolInfoDouble(symbol, SYMBOL_BID); //--- Get price double calculatedMargin; //--- Declare calculated margin bool success = OrderCalcMargin(orderType == ORDER_TYPE_BUY ? ORDER_TYPE_BUY : ORDER_TYPE_SELL, symbol, volume, price, calculatedMargin); //--- Calculate margin if (success) marginRequired = calculatedMargin; //--- Set margin if successful return AccountInfoDouble(ACCOUNT_MARGIN_FREE) - marginRequired; //--- Return free margin } //+------------------------------------------------------------------+ //| Check if trading is allowed | //+------------------------------------------------------------------+ bool isTradingAllowed() { bool isAllowed = false; //--- Initialize allowed flag return(true); //--- Return true }
这里,我们实现程序的工具函数,用于管理交易计数、持仓平仓、手数计算、保证金校验以及交易权限判断,保障交易处理的稳定性。首先,我们创建交易统计相关函数:使用"CountTrades"遍历PositionsTotal统计总持仓数量;通过PositionGetTicket获取有效订单票据,匹配"Symbol"与"magicNumber",对多单或空单分别累加"positionCount"计数;使用"CountTradesBuy"与"CountTradesSell"分别统计多单和空单持仓,通过POSITION_TYPE_BUY或"POSITION_TYPE_SELL"过滤持仓类型;使用"CheckBuyOrders"与"CheckSellOrders"检测是否存在对应magic数字的至少一笔多单或空单,找到第一条匹配持仓即终止循环;通过"CheckTotalBuyOrders"与"CheckTotalSellOrders"统计带有指定magic数字的全部多单、空单持仓数量;使用"CheckMarketBuyOrders"与"CheckMarketSellOrders"统计该magic数字下的多单或空单实际持仓。
接下来,我们继续实现"NormalizePrice"函数,使用NormalizeDouble按照_Digits对价格进行标准化处理;再实现"fnGetLotDigit"函数,依据SYMBOL_VOLUME_STEP返回手数对应的正确小数精度(例如手数步进为1时返回0,步进为0.1时返回1)。接下来,我们编写"CloseBuy"与"CloseSell"函数,用于平掉全部多单或空单:循环遍历全部持仓,校验"Symbol"与"magicNumber",使用"obj_Trade.PositionClose"执行平仓,直到"CheckMarketBuyOrders"或"CheckMarketSellOrders"返回0。最后,我们实现"GetLots" ,以根据复利百分比compoundPercent计算交易手数:计算公式为"AccountInfoDouble (ACCOUNT_BALANCE) * compoundPercent / 100 / 10000",通过"fnGetLotDigit"做精度标准化;结果受SYMBOL_VOLUME_MIN和"SYMBOL_VOLUME_MAX"约束;如果不启用复利,则使用初始手数"initialLotSize";实现"AccountFreeMarginCheck"函数:针对指定订单类型与交易量,调用OrderCalcMargin计算所需保证金,以此核算可用保证金,并实现占位函数"isTradingAllowed",直接返回true。为完成可视化效果,我们还需要编写在图表上绘制线条与文本标签的相关函数。
//+------------------------------------------------------------------+ //| Draw support/resistance line | //+------------------------------------------------------------------+ void MakeLine(double price) { string name = "level"; //--- Set line name if (ObjectFind(0, name) != -1) { //--- Check if line exists ObjectMove(0, name, 0, iTime(Symbol(), PERIOD_CURRENT, 0), price); //--- Move line return; //--- Exit function } ObjectCreate(0, name, OBJ_HLINE, 0, 0, price); //--- Create horizontal line ObjectSetInteger(0, name, OBJPROP_COLOR, lineColor); //--- Set color ObjectSetInteger(0, name, OBJPROP_STYLE, STYLE_SOLID); //--- Set style ObjectSetInteger(0, name, OBJPROP_WIDTH, lineWidth); //--- Set width ObjectSetInteger(0, name, OBJPROP_BACK, true); //--- Set to background } //+------------------------------------------------------------------+ //| Create dashboard label | //+------------------------------------------------------------------+ void LABEL(string labelName, string fontName, int fontSize, int xPosition, int yPosition, color textColor, int corner, string labelText) { if (ObjectFind(0, labelName) < 0) { //--- Check if label exists ObjectCreate(0, labelName, OBJ_LABEL, 0, 0, 0); //--- Create label } ObjectSetString(0, labelName, OBJPROP_TEXT, labelText); //--- Set label text ObjectSetString(0, labelName, OBJPROP_FONT, fontName); //--- Set font ObjectSetInteger(0, labelName, OBJPROP_FONTSIZE, fontSize); //--- Set font size ObjectSetInteger(0, labelName, OBJPROP_COLOR, textColor); //--- Set text color ObjectSetInteger(0, labelName, OBJPROP_CORNER, corner); //--- Set corner ObjectSetInteger(0, labelName, OBJPROP_XDISTANCE, xPosition); //--- Set x position ObjectSetInteger(0, labelName, OBJPROP_YDISTANCE, yPosition); //--- Set y position }
为给程序生成可视化元素,我们编写"MakeLine"函数,用于在指定"price"位置绘制水平线,标记支撑位或阻力位。将对象命名为"level",通过ObjectFind检测该绘图对象是否已存在:如果对象已存在,则调用ObjectMove,结合iTime获取当前K线时间,移动这条水平线;如果对象不存在,则调用ObjectCreate创建OBJ_HLINE对象。再通过ObjectSetInteger设置对象属性:设置"OBJPROP_COLOR"为"lineColor",设置OBJPROP_STYLE为"STYLE_SOLID",设置"OBJPROP_WIDTH"为"lineWidth"线宽,再将"OBJPROP_BACK"设置为true,并通过ObjectSetInteger将线条置于背景层。
接下来,我们实现"LABEL"函数,用于创建或更新仪表盘上的文本标签。先检测"labelName"标签对象是否存在,如果不存在,使用"ObjectCreate"创建 OBJ_LABEL文本标签对象;通过"ObjectSetString"配置字符串属性:设置"OBJPROP_TEXT"为"labelText",设置"OBJPROP_FONT"为"fontName";使用"ObjectSetInteger"配置整数属性:设置"OBJPROP_FONTSIZE"为"fontSize",设置"OBJPROP_COLOR"为"textColor",设置OBJPROP_CORNER为"corner",设置"OBJPROP_XDISTANCE"为"xPosition",并设置"OBJPROP_YDISTANCE"为Y轴偏移。完成后,我们就可以定义后续需要用到的指标工具函数。
//+------------------------------------------------------------------+ //| Calculate ATR indicator | //+------------------------------------------------------------------+ double MyiATR(string symbol, ENUM_TIMEFRAMES timeframe, int period, int shift) { int handle = iATR(symbol, timeframe, period); //--- Create ATR handle if (handle == INVALID_HANDLE) return 0; //--- Check invalid handle double buffer[1]; //--- Declare buffer if (CopyBuffer(handle, 0, shift, 1, buffer) != 1) buffer[0] = 0; //--- Copy ATR value IndicatorRelease(handle); //--- Release handle return buffer[0]; //--- Return ATR value } //+------------------------------------------------------------------+ //| Check bullish engulfing pattern | //+------------------------------------------------------------------+ bool BullishEngulfingExists() { if (iOpen(Symbol(), PERIOD_CURRENT, 1) <= iClose(Symbol(), PERIOD_CURRENT, 2) && iClose(Symbol(), PERIOD_CURRENT, 1) >= iOpen(Symbol(), PERIOD_CURRENT, 2) && iOpen(Symbol(), PERIOD_CURRENT, 2) - iClose(Symbol(), PERIOD_CURRENT, 2) >= 10 * _Point && iClose(Symbol(), PERIOD_CURRENT, 1) - iOpen(Symbol(), PERIOD_CURRENT, 1) >= 10 * _Point) { //--- Check bullish engulfing conditions return(true); //--- Return true } return(false); //--- Return false } //+------------------------------------------------------------------+ //| Check bullish harami pattern | //+------------------------------------------------------------------+ bool BullishHaramiExists() { if (iClose(Symbol(), PERIOD_CURRENT, 2) < iOpen(Symbol(), PERIOD_CURRENT, 2) && iOpen(Symbol(), PERIOD_CURRENT, 1) < iClose(Symbol(), PERIOD_CURRENT, 1) && iOpen(Symbol(), PERIOD_CURRENT, 2) - iClose(Symbol(), PERIOD_CURRENT, 2) > MyiATR(Symbol(), PERIOD_CURRENT, 14, 2) && iOpen(Symbol(), PERIOD_CURRENT, 2) - iClose(Symbol(), PERIOD_CURRENT, 2) > 4 * (iClose(Symbol(), PERIOD_CURRENT, 1) - iOpen(Symbol(), PERIOD_CURRENT, 1))) { //--- Check bullish harami conditions return(true); //--- Return true } return(false); //--- Return false } //+------------------------------------------------------------------+ //| Check doji at bottom pattern | //+------------------------------------------------------------------+ bool DojiAtBottomExists() { if (iOpen(Symbol(), PERIOD_CURRENT, 3) - iClose(Symbol(), PERIOD_CURRENT, 3) >= 8 * _Point && MathAbs(iClose(Symbol(), PERIOD_CURRENT, 2) - iOpen(Symbol(), PERIOD_CURRENT, 2)) <= 1 * _Point && iClose(Symbol(), PERIOD_CURRENT, 1) - iOpen(Symbol(), PERIOD_CURRENT, 1) >= 8 * _Point) { //--- Check doji at bottom conditions return(true); //--- Return true } return(false); //--- Return false } //+------------------------------------------------------------------+ //| Check doji at top pattern | //+------------------------------------------------------------------+ bool DojiAtTopExists() { if (iClose(Symbol(), PERIOD_CURRENT, 3) - iOpen(Symbol(), PERIOD_CURRENT, 3) >= 8 * _Point && MathAbs(iClose(Symbol(), PERIOD_CURRENT, 2) - iOpen(Symbol(), PERIOD_CURRENT, 2)) <= 1 * _Point && iOpen(Symbol(), PERIOD_CURRENT, 1) - iClose(Symbol(), PERIOD_CURRENT, 1) >= 8 * _Point) { //--- Check doji at top conditions return(true); //--- Return true } return(false); //--- Return false } //+------------------------------------------------------------------+ //| Check bearish harami pattern | //+------------------------------------------------------------------+ bool BearishHaramiExists() { if (iClose(Symbol(), PERIOD_CURRENT, 2) > iClose(Symbol(), PERIOD_CURRENT, 1) && iOpen(Symbol(), PERIOD_CURRENT, 2) < iOpen(Symbol(), PERIOD_CURRENT, 1) && iClose(Symbol(), PERIOD_CURRENT, 2) > iOpen(Symbol(), PERIOD_CURRENT, 2) && iOpen(Symbol(), PERIOD_CURRENT, 1) > iClose(Symbol(), PERIOD_CURRENT, 1) && iClose(Symbol(), PERIOD_CURRENT, 2) - iOpen(Symbol(), PERIOD_CURRENT, 2) > MyiATR(Symbol(), PERIOD_CURRENT, 14, 2) && iClose(Symbol(), PERIOD_CURRENT, 2) - iOpen(Symbol(), PERIOD_CURRENT, 2) > 4 * (iOpen(Symbol(), PERIOD_CURRENT, 1) - iClose(Symbol(), PERIOD_CURRENT, 1))) { //--- Check bearish harami conditions return(true); //--- Return true } return(false); //--- Return false } //+------------------------------------------------------------------+ //| Check long up candle pattern | //+------------------------------------------------------------------+ bool LongUpCandleExists() { if (iOpen(Symbol(), PERIOD_CURRENT, 2) < iClose(Symbol(), PERIOD_CURRENT, 2) && iHigh(Symbol(), PERIOD_CURRENT, 2) - iLow(Symbol(), PERIOD_CURRENT, 2) >= 40 * _Point && iHigh(Symbol(), PERIOD_CURRENT, 2) - iLow(Symbol(), PERIOD_CURRENT, 2) > 2.5 * MyiATR(Symbol(), PERIOD_CURRENT, 14, 2) && iClose(Symbol(), PERIOD_CURRENT, 1) < iOpen(Symbol(), PERIOD_CURRENT, 1) && iOpen(Symbol(), PERIOD_CURRENT, 1) - iClose(Symbol(), PERIOD_CURRENT, 1) > 10 * _Point) { //--- Check long up candle conditions return(true); //--- Return true } return(false); //--- Return false } //+------------------------------------------------------------------+ //| Check long down candle pattern | //+------------------------------------------------------------------+ bool LongDownCandleExists() { if (iOpen(Symbol(), PERIOD_CURRENT, 1) > iClose(Symbol(), PERIOD_CURRENT, 1) && iHigh(Symbol(), PERIOD_CURRENT, 1) - iLow(Symbol(), PERIOD_CURRENT, 1) >= 40 * _Point && iHigh(Symbol(), PERIOD_CURRENT, 1) - iLow(Symbol(), PERIOD_CURRENT, 1) > 2.5 * MyiATR(Symbol(), PERIOD_CURRENT, 14, 1)) { //--- Check long down candle conditions return(true); //--- Return true } return(false); //--- Return false } //+------------------------------------------------------------------+ //| Check bearish engulfing pattern | //+------------------------------------------------------------------+ bool BearishEngulfingExists() { if (iOpen(Symbol(), PERIOD_CURRENT, 1) >= iClose(Symbol(), PERIOD_CURRENT, 2) && iClose(Symbol(), PERIOD_CURRENT, 1) <= iOpen(Symbol(), PERIOD_CURRENT, 2) && iOpen(Symbol(), PERIOD_CURRENT, 2) - iClose(Symbol(), PERIOD_CURRENT, 2) >= 10 * _Point && iClose(Symbol(), PERIOD_CURRENT, 1) - iOpen(Symbol(), PERIOD_CURRENT, 1) >= 10 * _Point) { //--- Check bearish engulfing conditions return(true); //--- Return true } return(false); //--- Return false } //+------------------------------------------------------------------+ //| Calculate average range over 4 days | //+------------------------------------------------------------------+ double AveRange4() { double rangeSum = 0; //--- Initialize range sum int count = 0; //--- Initialize count int index = 1; //--- Initialize index while (count < 4) { //--- Loop until 4 days MqlDateTime dateTime; //--- Declare datetime structure TimeToStruct(iTime(Symbol(), PERIOD_CURRENT, index), dateTime); //--- Convert time if (dateTime.day_of_week != 0) { //--- Check non-Sunday rangeSum += iHigh(Symbol(), PERIOD_CURRENT, index) - iLow(Symbol(), PERIOD_CURRENT, index); //--- Add range count++; //--- Increment count } index++; //--- Increment index } return(rangeSum / 4.0); //--- Return average range } //+------------------------------------------------------------------+ //| Check buy pinbar | //+------------------------------------------------------------------+ bool IsBuyPinbar() { double currentOpen, currentClose, currentHigh, currentLow; //--- Declare current candle variables double previousHigh, previousLow, previousClose, previousOpen; //--- Declare previous candle variables double currentRange, previousRange, currentHigherPart, currentHigherPart1; //--- Declare range variables currentOpen = iOpen(Symbol(), PERIOD_CURRENT, 1); //--- Get current open currentClose = iClose(Symbol(), PERIOD_CURRENT, 1); //--- Get current close currentHigh = iHigh(Symbol(), PERIOD_CURRENT, 0); //--- Get current high currentLow = iLow(Symbol(), PERIOD_CURRENT, 1); //--- Get current low previousOpen = iOpen(Symbol(), PERIOD_CURRENT, 2); //--- Get previous open previousClose = iClose(Symbol(), PERIOD_CURRENT, 2); //--- Get previous close previousHigh = iHigh(Symbol(), PERIOD_CURRENT, 2); //--- Get previous high previousLow = iLow(Symbol(), PERIOD_CURRENT, 2); //--- Get previous low currentRange = currentHigh - currentLow; //--- Calculate current range previousRange = previousHigh - previousLow; //--- Calculate previous range currentHigherPart = currentHigh - currentRange * 0.4; //--- Calculate higher part currentHigherPart1 = currentHigh - currentRange * 0.4; //--- Calculate higher part double averageDailyRange = AveRange4(); //--- Get average daily range if ((currentClose > currentHigherPart1 && currentOpen > currentHigherPart) && //--- Check close/open in higher third (currentRange > averageDailyRange * 0.5) && //--- Check pinbar size (currentLow + currentRange * 0.25 < previousLow)) { //--- Check nose length double lowArray[3]; //--- Declare low array CopyLow(Symbol(), PERIOD_CURRENT, 3, 3, lowArray); //--- Copy low prices int minIndex = ArrayMinimum(lowArray); //--- Find minimum low index if (lowArray[minIndex] > currentLow) return(true); //--- Confirm buy pinbar } return(false); //--- Return false } //+------------------------------------------------------------------+ //| Check sell pinbar | //+------------------------------------------------------------------+ bool IsSellPinbar() { double currentOpen, currentClose, currentHigh, currentLow; //--- Declare current candle variables double previousHigh, previousLow, previousClose, previousOpen; //--- Declare previous candle variables double currentRange, previousRange, currentLowerPart, currentLowerPart1; //--- Declare range variables currentOpen = iOpen(Symbol(), PERIOD_CURRENT, 1); //--- Get current open currentClose = iClose(Symbol(), PERIOD_CURRENT, 1); //--- Get current close currentHigh = iHigh(Symbol(), PERIOD_CURRENT, 1); //--- Get current high currentLow = iLow(Symbol(), PERIOD_CURRENT, 1); //--- Get current low previousOpen = iOpen(Symbol(), PERIOD_CURRENT, 2); //--- Get previous open previousClose = iClose(Symbol(), PERIOD_CURRENT, 2); //--- Get previous close previousHigh = iHigh(Symbol(), PERIOD_CURRENT, 2); //--- Get previous high previousLow = iLow(Symbol(), PERIOD_CURRENT, 2); //--- Get previous low currentRange = currentHigh - currentLow; //--- Calculate current range previousRange = previousHigh - previousLow; //--- Calculate previous range currentLowerPart = currentLow + currentRange * 0.4; //--- Calculate lower part currentLowerPart1 = currentLow + currentRange * 0.4; //--- Calculate lower part double averageDailyRange = AveRange4(); //--- Get average daily range if ((currentClose < currentLowerPart1 && currentOpen < currentLowerPart) && //--- Check close/open in lower third (currentRange > averageDailyRange * 0.5) && //--- Check pinbar size (currentHigh - currentRange * 0.25 > previousHigh)) { //--- Check nose length double highArray[3]; //--- Declare high array CopyHigh(Symbol(), PERIOD_CURRENT, 3, 3, highArray); //--- Copy high prices int maxIndex = ArrayMaximum(highArray); //--- Find maximum high index if (highArray[maxIndex] < currentHigh) return(true); //--- Confirm sell pinbar } return(false); //--- Return false }
这里我们实现K线形态识别函数,同时为系统编写平均真实波幅(ATR)计算函数。首先编写"MyiATR"函数:调用iATR函数,根据传入的交易品种、时间周期与周期参数创建ATR指标句柄。如果句柄无效直接返回0;通过CopyBuffer将ATR数值读取至缓冲区;调用IndicatorRelease释放指标句柄,最后返回ATR值。
接下来,我们继续实现各类K线形态检测函数:通过"BullishEngulfingExists"判断是否出现看涨吞没形态,校验当前K线实体能够完全包裹前一根看跌K线,同时两根K线实体幅度足够大;使用"BullishHaramiExists"识别看涨孕线形态,小实体阳线被包裹在前一根大阴线内部,借助"MyiATR"做K线幅度对比;通过"DojiAtBottomExists"检测底部十字星,十字星K线介于阴线与阳线之间,构成早晨之星形态;使用"DojiAtTopExists"检测顶部十字星,十字星K线介于阳线与阴线之间,构成黄昏之星形态;通过"BearishHaramiExists"识别看跌孕线形态,小实体阴线被包裹在前一根大阳线内部;使用"LongUpCandleExists"判定强势大阳线,结合ATR阈值,且后一根K线为阴线;通过 "LongDownCandleExists"识别强势大阴线;使用"BearishEngulfingExists"校验看跌吞没形态,阴线实体完全包裹前一根阳线。
最后,我们实现"IsBuyPinbar"与"IsSellPinbar"两个函数,用于识别针形K线:校验当前K线的开盘价、收盘价处于K线波幅区间的上三分之一或下三分之一;K线总波幅要大于"AveRange4"(该指标对排除周日的 4 个交易日的高低点波幅取平均,得到日均波幅)的一半;针形K线的“鼻尖影线”需要突破前一根K线的低点或高点。通过CopyLow或 CopyHigh读取历史K线的最低或最高价格,再配合"ArrayMinimum"或ArrayMaximum函数求取数组极值,对比近期高低点完成条件确认。接下来,我们定义若干函数,用于显示当前的信号/形态信息,以及用于持仓管理的加权平均价格。
//+------------------------------------------------------------------+ //| Analyze candlestick patterns | //+------------------------------------------------------------------+ string CandleStick_Analyzer() { string candlePattern, comment1 = "", comment2 = "", comment3 = ""; //--- Initialize pattern strings string comment4 = "", comment5 = "", comment6 = "", comment7 = ""; //--- Initialize pattern strings string comment8 = "", comment9 = ""; //--- Initialize pattern strings if (BullishEngulfingExists()) comment1 = " Bullish Engulfing "; //--- Check bullish engulfing if (BullishHaramiExists()) comment2 = " Bullish Harami "; //--- Check bullish harami if (LongUpCandleExists()) comment3 = " Bullish LongUp "; //--- Check long up candle if (DojiAtBottomExists()) comment4 = " MorningStar Doji "; //--- Check morning star doji if (DojiAtTopExists()) comment5 = " EveningStar Doji "; //--- Check evening star doji if (BearishHaramiExists()) comment6 = " Bearish Harami "; //--- Check bearish harami if (BearishEngulfingExists()) comment7 = " Bearish Engulfing "; //--- Check bearish engulfing if (LongDownCandleExists()) comment8 = " Bearish LongDown "; //--- Check long down candle candlePattern = comment1 + comment2 + comment3 + comment4 + comment5 + comment6 + comment7 + comment8 + comment9; //--- Combine patterns return(candlePattern); //--- Return combined pattern } //+------------------------------------------------------------------+ //| Calculate average price for order type | //+------------------------------------------------------------------+ double rata_price(int orderType) { double totalVolume = 0; //--- Initialize total volume double weightedOpenSum = 0; //--- Initialize weighted open sum double averagePrice = 0; //--- Initialize average price for (int positionIndex = 0; positionIndex < PositionsTotal(); positionIndex++) { //--- Iterate through positions ulong ticket = PositionGetTicket(positionIndex); //--- Get position ticket if (ticket == 0) continue; //--- Skip invalid tickets if (PositionGetString(POSITION_SYMBOL) == Symbol() && PositionGetInteger(POSITION_MAGIC) == magicNumber && (PositionGetInteger(POSITION_TYPE) == orderType)) { //--- Check position match totalVolume += PositionGetDouble(POSITION_VOLUME); //--- Add volume weightedOpenSum += (PositionGetDouble(POSITION_VOLUME) * PositionGetDouble(POSITION_PRICE_OPEN)); //--- Add weighted open } } if (totalVolume != 0) { //--- Check non-zero volume averagePrice = weightedOpenSum / totalVolume; //--- Calculate average price } return(averagePrice); //--- Return average price }
为了进一步完善仓位管理,我们编写"CandleStick_Analyzer"函数:先初始化"comment1"至"comment9"共9个字符串变量,初始值置为空。调用前面已经定义好的各类形态检测函数,例如"BullishEngulfingExists";如果识别到对应K线形态,就将描述文本(例如"Bullish Engulfing")赋值给对应变量;再把所有识别到的形态文本拼接至"candlePattern",最终返回拼接后的完整字符串,以供给仪表盘展示。
接下来,我们实现"rata_price"函数,用于计算指定的"orderType"(做多或做空)的加权平均开仓价:初始化总成交量"totalVolume"与加权开仓金额总和"weightedOpenSum"为0;遍历"PositionsTotal"全部持仓;通过"PositionGetTicket"、 PositionGetString和"PositionGetInteger"获取持仓信息,筛选出匹配"Symbol"、"magicNumber"和"orderType"的持仓;累加POSITION_VOLUME,同时累加「"POSITION_VOLUME" × POSITION_PRICE_OPEN」;当"totalVolume"不为0时,根据公式"averagePrice" = "weightedOpenSum / totalVolume"计算加权平均价格并返回结果。该函数为交易信号的形态分析、均价加仓逻辑、止盈调整提供精准的平均价位数据支撑。处理持仓时,我们首先需要读取持仓的各项指标数据。让我们编写这部分逻辑。
//+------------------------------------------------------------------+ //| Calculate position metrics | //+------------------------------------------------------------------+ void calculatePositionMetrics() { buyCount = 0; //--- Reset buy count currentBuyLot = 0; //--- Reset current buy lot totalBuyLots = 0; //--- Reset total buy lots sellCount = 0; //--- Reset sell count currentSellLot = 0; //--- Reset current sell lot totalSellLots = 0; //--- Reset total sell lots totalSum = 0; //--- Reset total sum totalSwap = 0; //--- Reset total swap buyProfit = 0; //--- Reset buy profit sellProfit = 0; //--- Reset sell profit buyWeightedSum = 0; //--- Reset buy weighted sum sellWeightedSum = 0; //--- Reset sell weighted sum buyBreakEvenPrice = 0; //--- Reset buy breakeven price sellBreakEvenPrice = 0; //--- Reset sell breakeven price minBuyLot = 9999; //--- Initialize min buy lot minSellLot = 9999; //--- Initialize min sell lot maxSellPrice = 0; //--- Initialize max sell price minBuyPrice = 999999999; //--- Initialize min buy price for (int i = 0; i < PositionsTotal(); i++) { //--- Iterate through positions ulong ticket = PositionGetTicket(i); //--- Get position ticket if (ticket == 0) continue; //--- Skip invalid tickets if (PositionGetString(POSITION_SYMBOL) != Symbol()) continue; //--- Skip non-matching symbols if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) { //--- Check buy position buyCount++; //--- Increment buy count totalOperations++; //--- Increment total operations currentBuyLot = PositionGetDouble(POSITION_VOLUME); //--- Set current buy lot buyProfit += PositionGetDouble(POSITION_PROFIT); //--- Add buy profit totalBuyLots += PositionGetDouble(POSITION_VOLUME); //--- Add to total buy lots minBuyLot = MathMin(minBuyLot, PositionGetDouble(POSITION_VOLUME)); //--- Update min buy lot buyWeightedSum += PositionGetDouble(POSITION_VOLUME) * PositionGetDouble(POSITION_PRICE_OPEN); //--- Add weighted open price minBuyPrice = MathMin(minBuyPrice, PositionGetDouble(POSITION_PRICE_OPEN)); //--- Update min buy price } if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL) { //--- Check sell position sellCount++; //--- Increment sell count totalOperations++; //--- Increment total operations currentSellLot = PositionGetDouble(POSITION_VOLUME); //--- Set current sell lot sellProfit += PositionGetDouble(POSITION_PROFIT); //--- Add sell profit totalSellLots += PositionGetDouble(POSITION_VOLUME); //--- Add to total sell lots minSellLot = MathMin(minSellLot, PositionGetDouble(POSITION_VOLUME)); //--- Update min sell lot sellWeightedSum += PositionGetDouble(POSITION_VOLUME) * PositionGetDouble(POSITION_PRICE_OPEN); //--- Add weighted open price maxSellPrice = MathMax(maxSellPrice, PositionGetDouble(POSITION_PRICE_OPEN)); //--- Update max sell price } } if (totalBuyLots > 0) { //--- Check buy lots buyBreakEvenPrice = buyWeightedSum / totalBuyLots; //--- Calculate buy breakeven } if (totalSellLots > 0) { //--- Check sell lots sellBreakEvenPrice = sellWeightedSum / totalSellLots; //--- Calculate sell breakeven } }
为高效管理多持仓并计算核心统计指标,我们实现"calculatePositionMetrics"函数。首先,将关键变量重置为0或对应初始值,保证统计数据准确。随后,我们通过PositionsTotal遍历全部持仓,调用PositionGetTicket获取每一笔持仓的票据编号,跳过无效票据,再通过PositionGetString过滤掉品种不匹配的持仓。针对多头持仓(POSITION_TYPE_BUY):将"buyCount"与"totalOperations"累加,赋值"currentBuyLot",再将"POSITION_PROFIT"累加至"buyProfit","POSITION_VOLUME"累加至"totalBuyLots";通过MathMin更新"minBuyLot";把加权开仓金额计入"buyWeightedSum",并更新"minBuyPrice";针对空头持仓(POSITION_TYPE_SELL),对空单相关指标执行同样的更新逻辑。最后,如果"totalBuyLots"大于0,则按"buyWeightedSum / totalBuyLots"计算"buyBreakEvenPrice";如果"totalSellLots"大于0,则按"sellWeightedSum / totalSellLots"计算"sellBreakEvenPrice"。输出的加权平均开仓价用于盈亏平衡管理,为均价加仓策略与风控模块提供精准的持仓统计数据。完成以上函数后,我们就可以编写开仓逻辑,该部分将在OnTick事件处理器内实现。
//+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { static datetime previousBarTime = 0; //--- Store previous bar time if (previousBarTime != iTime(Symbol(), PERIOD_CURRENT, 0)) { //--- Check new bar previousBarTime = iTime(Symbol(), PERIOD_CURRENT, 0); //--- Update previous bar time ChartRedraw(0); //--- Redraw chart } else { return; //--- Exit if not new bar } if (iVolume(Symbol(), PERIOD_H4, 0) > iVolume(Symbol(), PERIOD_H4, 1)) return; //--- Exit if volume increased double supportResistanceLevel = NormalizeDouble(iClose(Symbol(), PERIOD_H4, 1), _Digits); //--- Get support/resistance level ObjectDelete(0, "level"); //--- Delete existing level line MakeLine(supportResistanceLevel); //--- Draw support/resistance line if (SymbolInfoInteger(Symbol(), SYMBOL_SPREAD) > 150) return; //--- Exit if spread too high int totalBuyPositions = 0; //--- Initialize buy positions count int totalSellPositions = 0; //--- Initialize sell positions count for (int i = 0; i < PositionsTotal(); i++) { //--- Iterate through positions ulong ticket = PositionGetTicket(i); //--- Get position ticket if (ticket == 0) continue; //--- Skip invalid tickets if (PositionGetString(POSITION_SYMBOL) != Symbol() || PositionGetInteger(POSITION_MAGIC) != magicNumber) continue; //--- Skip non-matching positions if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) { //--- Check buy position totalBuyPositions++; //--- Increment buy count } if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL) { //--- Check sell position totalSellPositions++; //--- Increment sell count } } }
在OnTick事件处理器中,我们实现针形K线均价加仓系统的基础逻辑,每一根新K线生成时处理交易决策与图表可视化刷新。首先,我们检测是否产生新K线:将静态变量"previousBarTime"(初始值为0),与通过iTime获取的当前品种和当前周期0号偏移K线时间做对比,如果检测到新K线,则更新"previousBarTime",并调用ChartRedraw刷新图表;如果未生成新K线,则直接退出本次执行。
接下来,如果通过iVolume获取的当前H4周期K线成交量大于前一根K线成交量,则直接退出本次循环,以此规避高波动行情时段。接下来,我们读取前一根H4周期K线的收盘价,调用iClose获取价格,并通过NormalizeDouble进行标准化处理,计算得到支撑/阻力位;调用ObjectDelete删除名称为"level"的旧水平线,再通过"MakeLine"在该价位绘制新的水平标记。最后,我们通过SymbolInfoInteger读取点差,如果点差超过150点,则直接退出执行。遍历PositionsTotal统计当前持仓:借助"PositionGetTicket" 获取持仓票据,跳过无效票据、品种不匹配以及"magicNumber"不一致的持仓;通过PositionGetInteger函数识别持仓方向,分别累加"totalBuyPositions"或"totalSellPositions"。以上初始化逻辑保证EA仅在K线更新且市场条件适宜的情况下执行交易逻辑,同时图表上始终保留最新的可视化价位参考。编译后,我们得到以下结果:

由图可见,我们已经在图表上动态标记出了支撑位与阻力位。现在我们要实现动态加仓与开仓逻辑。
if (CheckMarketBuyOrders() < 70 && CheckMarketSellOrders() < 70) { //--- Check order limits if (supportResistanceLevel > iOpen(Symbol(), PERIOD_CURRENT, 0) && useSignalMode == DISABLED) { //--- Check buy condition if (IsBuyPinbar() && totalBuyPositions < maxOrders && (isTradingAllowed() || totalBuyPositions > 0)) { //--- Check buy pinbar and limits double buyStopLoss = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_ASK) - stopLossPips * normalizedPoint, _Digits); //--- Calculate buy stop loss double buyTakeProfit = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_ASK) + takeProfitPips * normalizedPoint, _Digits); //--- Calculate buy take profit if (AccountFreeMarginCheck(Symbol(), ORDER_TYPE_BUY, GetLots()) > 0) { //--- Check margin obj_Trade.PositionOpen(Symbol(), ORDER_TYPE_BUY, GetLots(), SymbolInfoDouble(_Symbol, SYMBOL_ASK), buyStopLoss, buyTakeProfit, orderComment); //--- Open buy position if (useAutoTakeProfit) { //--- Check auto take profit ModifyTP(ORDER_TYPE_BUY, rata_price(ORDER_TYPE_BUY) + takeProfitPips * normalizedPoint); //--- Modify take profit } CloseSell(); //--- Close sell positions } } } if (supportResistanceLevel < iOpen(Symbol(), PERIOD_CURRENT, 0) && useSignalMode == DISABLED) { //--- Check sell condition if (IsSellPinbar() && totalSellPositions < maxOrders && (isTradingAllowed() || totalSellPositions > 0)) { //--- Check sell pinbar and limits double sellStopLoss = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_BID) + stopLossPips * normalizedPoint, _Digits); //--- Calculate sell stop loss double sellTakeProfit = NormalizeDouble(SymbolInfoDouble(_Symbol, SYMBOL_BID) - takeProfitPips * normalizedPoint, _Digits); //--- Calculate sell take profit if (AccountFreeMarginCheck(Symbol(), ORDER_TYPE_SELL, GetLots()) > 0) { //--- Check margin obj_Trade.PositionOpen(Symbol(), ORDER_TYPE_SELL, GetLots(), SymbolInfoDouble(_Symbol, SYMBOL_BID), sellStopLoss, sellTakeProfit, orderComment); //--- Open sell position if (useAutoTakeProfit) { //--- Check auto take profit ModifyTP(ORDER_TYPE_SELL, rata_price(ORDER_TYPE_SELL) - takeProfitPips * normalizedPoint); //--- Modify take profit } CloseBuy(); //--- Close buy positions } } } } if (CountTrades() == 0) { //--- Check no trades if (supportResistanceLevel > iOpen(Symbol(), PERIOD_CURRENT, 0) && useSignalMode == ENABLED) { //--- Check buy signal mode if (IsBuyPinbar() && CountTrades() < maxOrders) { //--- Check buy pinbar and limit obj_Trade.PositionOpen(Symbol(), ORDER_TYPE_BUY, GetLots(), SymbolInfoDouble(_Symbol, SYMBOL_ASK), SymbolInfoDouble(_Symbol, SYMBOL_ASK) - stopLossPips * normalizedPoint, SymbolInfoDouble(_Symbol, SYMBOL_ASK) + (takeProfitPips * normalizedPoint), orderComment); //--- Open buy position } } } if (CountTrades() == 0) { //--- Check no trades if (supportResistanceLevel < iOpen(Symbol(), PERIOD_CURRENT, 0) && useSignalMode == ENABLED) { //--- Check sell signal mode if (IsSellPinbar() && CountTrades() < maxOrders) { //--- Check sell pinbar and limit obj_Trade.PositionOpen(Symbol(), ORDER_TYPE_SELL, GetLots(), SymbolInfoDouble(_Symbol, SYMBOL_BID), SymbolInfoDouble(_Symbol, SYMBOL_BID) + stopLossPips * normalizedPoint, SymbolInfoDouble(_Symbol, SYMBOL_BID) - (takeProfitPips * normalizedPoint), orderComment); //--- Open sell position } } }
我们继续完善OnTick函数的实现,新增基于针形K线信号与市场环境条件的开仓逻辑。首先,通过"CheckMarketBuyOrders"和"CheckMarketSellOrders"校验当前多空持仓数量均小于70,确保EA不会超出实际运行上限。接下来,当"useSignalMode"为"DISABLED"时,校验做多入场条件:"supportResistanceLevel"高于iOpen获取的当前K线开盘价;"IsBuyPinbar"识别到看涨针形K线;"totalBuyPositions"小于参数"maxOrders";并且"isTradingAllowed"允许交易或者已经存在多单持仓。满足条件后,借助SymbolInfoDouble读取品种属性,结合"stopLossPips"、"takeProfitPips"以及"normalizedPoint"换算,计算"buyStopLoss"和"buyTakeProfit";通过"AccountFreeMarginCheck"校验保证金充足;
使用"GetLots"结合"obj_Trade.PositionOpen"开多仓;如果"useAutoTakeProfit"为true,则通过"ModifyTP"修改止盈;同时执行"CloseSell"平空仓。做空条件逻辑与之相似:当"supportResistanceLevel"低于K线开盘价,且"IsSellPinbar"识别到看跌针形K线,则触发做空流程。接下来,当"CountTrades"为0且"useSignalMode"为"ENABLED"时,在检测到看涨针形K线("IsBuyPinbar")并且"CountTrades"低于"maxOrders"时,使用"obj_Trade.PositionOpen"开立买入仓位,并设置计算好的止损和止盈;同理,在出现看跌针形K线("IsSellPinbar")时开立卖出仓位,从而确保EA基于关键价位的针形K线信号,以适当的风险管理开立仓位。编译后,我们得到以下结果:

既然我们已经完成信号的校验与开仓逻辑,就需要对交易信号进行管理。为此,我们将编写若干专门的函数来实现该功能。
//+------------------------------------------------------------------+ //| Update stop loss and take profit | //+------------------------------------------------------------------+ void updateStopLossTakeProfit() { for (int i = PositionsTotal() - 1; i >= 0; i--) { //--- Iterate through positions ulong ticket = PositionGetTicket(i); //--- Get position ticket if (ticket == 0) continue; //--- Skip invalid tickets if (PositionGetString(POSITION_SYMBOL) != Symbol()) continue; //--- Skip non-matching symbols if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) { //--- Check buy position double buyTakeProfitLevel = (buyBreakEvenPrice + takeProfitPips * _Point) * (takeProfitPips > 0); //--- Calculate buy take profit double buyStopLossLevel = PositionGetDouble(POSITION_SL); //--- Get current stop loss if (slBreakevenMinus > 0) { //--- Check breakeven adjustment buyStopLossLevel = (buyBreakEvenPrice - slBreakevenMinus * _Point); //--- Set breakeven stop loss } if (buyCount == 1) { //--- Check single buy position buyTakeProfitLevel = NormalizePrice(PositionGetDouble(POSITION_PRICE_OPEN) + takeProfitPips * _Point) * (takeProfitPips > 0); //--- Set take profit if (laterUseSL > 0) { //--- Check unused stop loss buyStopLossLevel = (PositionGetDouble(POSITION_PRICE_OPEN) - laterUseSL * _Point); //--- Set stop loss } } buyTakeProfitLevel = NormalizePrice(buyTakeProfitLevel); //--- Normalize take profit buyStopLossLevel = NormalizePrice(buyStopLossLevel); //--- Normalize stop loss if (SymbolInfoDouble(_Symbol, SYMBOL_BID) >= buyTakeProfitLevel && buyTakeProfitLevel > 0) { //--- Check take profit hit obj_Trade.PositionClose(ticket); //--- Close position } if (SymbolInfoDouble(_Symbol, SYMBOL_BID) <= buyStopLossLevel) { //--- Check stop loss hit obj_Trade.PositionClose(ticket); //--- Close position } if (NormalizePrice(PositionGetDouble(POSITION_TP)) != buyTakeProfitLevel || NormalizePrice(PositionGetDouble(POSITION_SL)) != buyStopLossLevel) { //--- Check modification needed obj_Trade.PositionModify(ticket, buyStopLossLevel, buyTakeProfitLevel); //--- Modify position } } if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL) { //--- Check sell position double sellTakeProfitLevel = (sellBreakEvenPrice - takeProfitPips * _Point) * (takeProfitPips > 0); //--- Calculate sell take profit double sellStopLossLevel = PositionGetDouble(POSITION_SL); //--- Get current stop loss if (slBreakevenMinus > 0) { //--- Check breakeven adjustment sellStopLossLevel = (sellBreakEvenPrice + slBreakevenMinus * _Point); //--- Set breakeven stop loss } if (sellCount == 1) { //--- Check single sell position sellTakeProfitLevel = (PositionGetDouble(POSITION_PRICE_OPEN) - takeProfitPips * _Point) * (takeProfitPips > 0); //--- Set take profit if (laterUseSL > 0) { //--- Check unused stop loss sellStopLossLevel = (PositionGetDouble(POSITION_PRICE_OPEN) + laterUseSL * _Point); //--- Set stop loss } } sellTakeProfitLevel = NormalizePrice(sellTakeProfitLevel); //--- Normalize take profit sellStopLossLevel = NormalizePrice(sellStopLossLevel); //--- Normalize stop loss if (SymbolInfoDouble(_Symbol, SYMBOL_ASK) <= sellTakeProfitLevel) { //--- Check take profit hit obj_Trade.PositionClose(ticket); //--- Close position } if (SymbolInfoDouble(_Symbol, SYMBOL_ASK) >= sellStopLossLevel && sellStopLossLevel > 0) { //--- Check stop loss hit obj_Trade.PositionClose(ticket); //--- Close position } if (NormalizePrice(PositionGetDouble(POSITION_TP)) != sellTakeProfitLevel || NormalizePrice(PositionGetDouble(POSITION_SL)) != sellStopLossLevel) { //--- Check modification needed obj_Trade.PositionModify(ticket, sellStopLossLevel, sellTakeProfitLevel); //--- Modify position } } } } //+------------------------------------------------------------------+ //| Add averaging order | //+------------------------------------------------------------------+ void addAveragingOrder() { int positionIndex = 0; //--- Initialize position index double lastOpenPrice = 0; //--- Initialize last open price double lastLotSize = 0; //--- Initialize last lot size bool isLastBuy = false; //--- Initialize buy flag int totalBuyPositions = 0; //--- Initialize buy positions count int totalSellPositions = 0; //--- Initialize sell positions count long currentSpread = SymbolInfoInteger(Symbol(), SYMBOL_SPREAD); //--- Get current spread double supportResistanceLevel = iClose(Symbol(), PERIOD_H4, 1); //--- Get support/resistance level for (positionIndex = 0; positionIndex < PositionsTotal(); positionIndex++) { //--- Iterate through positions ulong ticket = PositionGetTicket(positionIndex); //--- Get position ticket if (ticket == 0) continue; //--- Skip invalid tickets if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY && PositionGetString(POSITION_SYMBOL) == Symbol() && PositionGetInteger(POSITION_MAGIC) == magicNumber) { //--- Check buy position if (lastOpenPrice == 0) { //--- Check initial price lastOpenPrice = PositionGetDouble(POSITION_PRICE_OPEN); //--- Set initial price } if (lastOpenPrice > PositionGetDouble(POSITION_PRICE_OPEN)) { //--- Check lower price lastOpenPrice = PositionGetDouble(POSITION_PRICE_OPEN); //--- Update last price } if (lastLotSize < PositionGetDouble(POSITION_VOLUME)) { //--- Check larger lot lastLotSize = PositionGetDouble(POSITION_VOLUME); //--- Update lot size } isLastBuy = true; //--- Set buy flag totalBuyPositions++; //--- Increment buy count } if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL && PositionGetString(POSITION_SYMBOL) == Symbol() && PositionGetInteger(POSITION_MAGIC) == magicNumber) { //--- Check sell position if (lastOpenPrice == 0) { //--- Check initial price lastOpenPrice = PositionGetDouble(POSITION_PRICE_OPEN); //--- Set initial price } if (lastOpenPrice < PositionGetDouble(POSITION_PRICE_OPEN)) { //--- Check higher price lastOpenPrice = PositionGetDouble(POSITION_PRICE_OPEN); //--- Update last price } if (lastLotSize < PositionGetDouble(POSITION_VOLUME)) { //--- Check larger lot lastLotSize = PositionGetDouble(POSITION_VOLUME); //--- Update lot size } isLastBuy = false; //--- Clear buy flag totalSellPositions++; //--- Increment sell count } } if (isLastBuy) { //--- Check buy position if (supportResistanceLevel > iOpen(Symbol(), PERIOD_CURRENT, 0)) { //--- Check buy condition if (IsBuyPinbar() && SymbolInfoDouble(_Symbol, SYMBOL_BID) <= lastOpenPrice - (orderDistancePips * _Point)) { //--- Check buy pinbar and distance obj_Trade.PositionOpen(Symbol(), ORDER_TYPE_BUY, NormalizeDouble((lastLotSize * lotMultiplier), fnGetLotDigit()), SymbolInfoDouble(_Symbol, SYMBOL_ASK), SymbolInfoDouble(_Symbol, SYMBOL_ASK) - stopLossPips * normalizedPoint, SymbolInfoDouble(_Symbol, SYMBOL_ASK) + (takeProfitPips * normalizedPoint), orderComment); //--- Open buy position isLastBuy = false; //--- Clear buy flag return; //--- Exit function } } } else if (!isLastBuy) { //--- Check sell position if (supportResistanceLevel < iOpen(Symbol(), PERIOD_CURRENT, 0)) { //--- Check sell condition if (IsSellPinbar() && SymbolInfoDouble(_Symbol, SYMBOL_ASK) >= lastOpenPrice + (orderDistancePips * _Point)) { //--- Check sell pinbar and distance obj_Trade.PositionOpen(Symbol(), ORDER_TYPE_SELL, NormalizeDouble((lastLotSize * lotMultiplier), fnGetLotDigit()), SymbolInfoDouble(_Symbol, SYMBOL_BID), SymbolInfoDouble(_Symbol, SYMBOL_BID) + stopLossPips * normalizedPoint, SymbolInfoDouble(_Symbol, SYMBOL_BID) - (takeProfitPips * normalizedPoint), orderComment); //--- Open sell position return; //--- Exit function } } } } //+------------------------------------------------------------------+ //| Add averaging order with auto take profit | //+------------------------------------------------------------------+ void addAveragingOrderWithAutoTP() { int positionIndex = 0; //--- Initialize position index double lastOpenPrice = 0; //--- Initialize last open price double lastLotSize = 0; //--- Initialize last lot size bool isLastBuy = false; //--- Initialize buy flag int totalBuyPositions = 0; //--- Initialize buy positions count int totalSellPositions = 0; //--- Initialize sell positions count long currentSpread = SymbolInfoInteger(Symbol(), SYMBOL_SPREAD); //--- Get current spread double supportResistanceLevel = iClose(Symbol(), PERIOD_H4, 1); //--- Get support/resistance level for (positionIndex = 0; positionIndex < PositionsTotal(); positionIndex++) { //--- Iterate through positions ulong ticket = PositionGetTicket(positionIndex); //--- Get position ticket if (ticket == 0) continue; //--- Skip invalid tickets if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY && PositionGetString(POSITION_SYMBOL) == Symbol() && PositionGetInteger(POSITION_MAGIC) == magicNumber) { //--- Check buy position if (lastOpenPrice == 0) { //--- Check initial price lastOpenPrice = PositionGetDouble(POSITION_PRICE_OPEN); //--- Set initial price } if (lastOpenPrice > PositionGetDouble(POSITION_PRICE_OPEN)) { //--- Check lower price lastOpenPrice = PositionGetDouble(POSITION_PRICE_OPEN); //--- Update last price } if (lastLotSize < PositionGetDouble(POSITION_VOLUME)) { //--- Check larger lot lastLotSize = PositionGetDouble(POSITION_VOLUME); //--- Update lot size } isLastBuy = true; //--- Set buy flag totalBuyPositions++; //--- Increment buy count } if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL && PositionGetString(POSITION_SYMBOL) == Symbol() && PositionGetInteger(POSITION_MAGIC) == magicNumber) { //--- Check sell position if (lastOpenPrice == 0) { //--- Check initial price lastOpenPrice = PositionGetDouble(POSITION_PRICE_OPEN); //--- Set initial price } if (lastOpenPrice < PositionGetDouble(POSITION_PRICE_OPEN)) { //--- Check higher price lastOpenPrice = PositionGetDouble(POSITION_PRICE_OPEN); //--- Update last price } if (lastLotSize < PositionGetDouble(POSITION_VOLUME)) { //--- Check larger lot lastLotSize = PositionGetDouble(POSITION_VOLUME); //--- Update lot size } isLastBuy = false; //--- Clear buy flag totalSellPositions++; //--- Increment sell count } } if (isLastBuy) { //--- Check buy position if (supportResistanceLevel > iOpen(Symbol(), PERIOD_CURRENT, 0)) { //--- Check buy condition if (IsBuyPinbar() && SymbolInfoDouble(_Symbol, SYMBOL_BID) <= lastOpenPrice - (orderDistancePips * _Point)) { //--- Check buy pinbar and distance obj_Trade.PositionOpen(Symbol(), ORDER_TYPE_BUY, NormalizeDouble((lastLotSize * lotMultiplier), fnGetLotDigit()), SymbolInfoDouble(_Symbol, SYMBOL_ASK), 0, 0, orderComment); //--- Open buy position calculatePositionMetrics(); //--- Calculate position metrics updateStopLossTakeProfit(); //--- Update stop loss and take profit isLastBuy = false; //--- Clear buy flag return; //--- Exit function } } } else if (!isLastBuy) { //--- Check sell position if (supportResistanceLevel < iOpen(Symbol(), PERIOD_CURRENT, 0)) { //--- Check sell condition if (IsSellPinbar() && SymbolInfoDouble(_Symbol, SYMBOL_ASK) >= lastOpenPrice + (orderDistancePips * _Point)) { //--- Check sell pinbar and distance obj_Trade.PositionOpen(Symbol(), ORDER_TYPE_SELL, NormalizeDouble((lastLotSize * lotMultiplier), fnGetLotDigit()), SymbolInfoDouble(_Symbol, SYMBOL_BID), 0, 0, orderComment); //--- Open sell position calculatePositionMetrics(); //--- Calculate position metrics updateStopLossTakeProfit(); //--- Update stop loss and take profit return; //--- Exit function } } } }
这里,我们实现"updateStopLossTakeProfit"、"addAveragingOrder"以及"addAveragingOrderWithAutoTP"函数,用来管理止损、止盈以及均价加仓交易,以实现持仓参数的动态调整。首先,我们编写"updateStopLossTakeProfit"函数,用于遍历全部持仓。针对多头持仓(POSITION_TYPE_BUY):如果"takeProfitPips"大于0,则基于"buyBreakEvenPrice"加上"takeProfitPips * _Point",计算出"buyTakeProfitLevel",通过PositionGetDouble获得当前持仓止损,如果"slBreakevenMinus"大于0,则将止损调整"buyBreakEvenPrice - slBreakevenMinus * _Point";如果为单笔持仓("buyCount"为1),则基于POSITION_PRICE_OPEN开仓价,叠加止盈、止损点数来设置止盈与止损。通过"NormalizePrice"对止损、止盈价位进行标准化处理,如果bid价触及止盈或止损,调用"obj_Trade.PositionClose"执行平仓;如果新旧风控价位不一致,则通过"obj_Trade.PositionModify"修改持仓的止损止盈。空头持仓执行相似逻辑,使用"sellBreakEvenPrice"和ask价进行计算。
接下来,我们实现"addAveragingOrder"函数,函数通过遍历PositionsTotal全部持仓来定位最新持仓:更新"lastOpenPrice"为多单最低开仓价或空单最高开仓价,"lastLotSize"更新为对应最大持仓手数,并同步设置"isLastBuy"标记用以区分最新持仓是多单还是空单。对于多单,如果"supportResistanceLevel"高于当前K线开盘价、"IsBuyPinbar"检测到看涨针形K线,并且bid价相比"lastOpenPrice"向下偏离达到"orderDistancePips * _Point",我们通过"obj_Trade.PositionOpen"开多仓,加仓手数 = "lastLotSize * lotMultiplier",并经由"fnGetLotDigit"对手数做精度标准化,同时带入预先计算好的止损和止盈,执行完毕后清空"isLastBuy"标记。对于空单,校验ask价相比"lastOpenPrice"向上偏离达到"orderDistancePips * _Point",条件成立则以相同逻辑开空仓。
最后实现"addAveragingOrderWithAutoTP"函数,内部逻辑与"addAveragingOrder"大体一致,但开仓时不设置初始止损与止盈(参数置0);开仓完成后调用"calculatePositionMetrics"刷新持仓统计指标(例如多单盈亏平衡价"buyBreakEvenPrice"),再调用"updateStopLossTakeProfit",基于盈亏平衡成本自动生成止损止盈价位,以此实现均价加仓后的动态风控调整。至此,我们可以调用以上函数,使整套加仓逻辑正式生效。
if (useSignalMode == ENABLED && CountTradesBuy() >= 1 && CountTradesBuy() < maxOrders && useAutoTakeProfit == false) { //--- Check buy averaging addAveragingOrder(); //--- Add buy averaging order } if (useSignalMode == ENABLED && CountTradesSell() >= 1 && CountTradesSell() < maxOrders && useAutoTakeProfit == false) { //--- Check sell averaging addAveragingOrder(); //--- Add sell averaging order } if (useSignalMode == ENABLED && CountTradesBuy() >= 1 && CountTradesBuy() < maxOrders && useAutoTakeProfit == true) { //--- Check buy averaging with auto TP addAveragingOrderWithAutoTP(); //--- Add buy averaging order with auto TP } if (useSignalMode == ENABLED && CountTradesSell() >= 1 && CountTradesSell() < maxOrders && useAutoTakeProfit == true) { //--- Check sell averaging with auto TP addAveragingOrderWithAutoTP(); //--- Add sell averaging order with auto TP }
我们继续完成OnTick事件逻辑,增加特定条件下的均价加仓处理逻辑,以此提升EA动态分批加仓的能力。首先,当"useSignalMode"为"ENABLED"时,通过"CountTradesBuy"判断至少存在一笔多单持仓,且多单数量未超过"maxOrders";如果"useAutoTakeProfit"为关闭状态,我们则调用"addAveragingOrder",依据针形K线识别结果与价格间隔条件,以放大后的手数新增一笔多单。
随后,我们对空单执行相同的逻辑:通过"CountTradesSell"校验空单持仓,当"useAutoTakeProfit"为false时,调用"addAveragingOrder",在满足条件下新增空单。接下来,轮到"useAutoTakeProfit"为true时的多单场景:我们调用"addAveragingOrderWithAutoTP"执行多单加仓,开仓时不设置初始止损或止盈;函数内部会自动刷新持仓统计指标,并基于盈亏平衡成本调整风控价位。最后,对于"useAutoTakeProfit"开启时的空单也进行同样的处理,调用 "addAveragingOrderWithAutoTP"新增空单,实现止损和止盈的动态调整。该逻辑确保EA在信号模式下可以妥善管理均价加仓,跟随市场行情变化做出适配。编译后,呈现如下效果:

现在,我们已经实现了均价加仓功能,剩下就是增加用于风险管理的移动止损逻辑。想要做到风险控制,移动止损逻辑需要每个Tick都执行,所以我们把这部分代码写在新K线判断逻辑的外面。
double setPointValue = normalizedPoint; //--- Set point value for calculations if (useTrailingStop && trailingStartPips > 0 && breakevenPips < trailingStartPips) { //--- Check trailing stop conditions double averageBuyPrice = rata_price(ORDER_TYPE_BUY); //--- Calculate average buy price double trailingReference = 0; //--- Initialize trailing reference for (int iTrade = 0; iTrade < PositionsTotal(); iTrade++) { //--- Iterate through positions ulong ticket = PositionGetTicket(iTrade); //--- Get position ticket if (ticket == 0) continue; //--- Skip invalid tickets if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY && PositionGetString(POSITION_SYMBOL) == Symbol() && PositionGetInteger(POSITION_MAGIC) == magicNumber) { //--- Check buy position if (useAutoTakeProfit) { //--- Check auto take profit trailingReference = averageBuyPrice; //--- Use average buy price } else { //--- Use open price trailingReference = PositionGetDouble(POSITION_PRICE_OPEN); //--- Set open price } if (SymbolInfoDouble(_Symbol, SYMBOL_BID) - trailingReference > trailingStartPips * setPointValue) { //--- Check trailing condition if (SymbolInfoDouble(_Symbol, SYMBOL_BID) - ((trailingStartPips - breakevenPips) * setPointValue) > PositionGetDouble(POSITION_SL)) { //--- Check stop loss adjustment obj_Trade.PositionModify(ticket, SymbolInfoDouble(_Symbol, SYMBOL_BID) - ((trailingStartPips - breakevenPips) * setPointValue), PositionGetDouble(POSITION_TP)); //--- Modify position } } } } double averageSellPrice = rata_price(ORDER_TYPE_SELL); //--- Calculate average sell price for (int iTrade2 = 0; iTrade2 < PositionsTotal(); iTrade2++) { //--- Iterate through positions ulong ticket2 = PositionGetTicket(iTrade2); //--- Get position ticket if (ticket2 == 0) continue; //--- Skip invalid tickets if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL && PositionGetString(POSITION_SYMBOL) == Symbol() && PositionGetInteger(POSITION_MAGIC) == magicNumber) { //--- Check sell position if (useAutoTakeProfit) { //--- Check auto take profit trailingReference = averageSellPrice; //--- Use average sell price } else { //--- Use open price trailingReference = PositionGetDouble(POSITION_PRICE_OPEN); //--- Set open price } if (trailingReference - SymbolInfoDouble(_Symbol, SYMBOL_ASK) > trailingStartPips * setPointValue) { //--- Check trailing condition if (SymbolInfoDouble(_Symbol, SYMBOL_ASK) + ((trailingStartPips - breakevenPips) * setPointValue) < PositionGetDouble(POSITION_SL) || PositionGetDouble(POSITION_SL) == 0) { //--- Check stop loss adjustment obj_Trade.PositionModify(ticket2, SymbolInfoDouble(_Symbol, SYMBOL_ASK) + ((trailingStartPips - breakevenPips) * setPointValue), PositionGetDouble(POSITION_TP)); //--- Modify position } } } } }
我们实现移动止损逻辑:首先将"setPointValue"赋值为"normalizedPoint",保证价格计算口径统一;接着校验条件:当"useTrailingStop"为true、"trailingStartPips"大于0,且"breakevenPips"小于"trailingStartPips",以此保障移动止损触发条件合法有效。接下来,我们处理多仓:使用"rata_price"传入ORDER_TYPE_BUY,计算多单"averageBuyPrice";遍历全部持仓,筛选出品种与魔术数相匹配的有效多单持仓票据。如果"useAutoTakeProfit"为true,则将"trailingReference"设置为"averageBuyPrice";否则取单笔持仓开仓价"POSITION_PRICE_OPEN" 。当bid价相对"trailingReference"向上超出"trailingStartPips * setPointValue",并且新止损位高于持仓当前止损时,则通过"obj_Trade.PositionModify"将止损修改为SYMBOL_BID − "(trailingStartPips − breakevenPips) * setPointValue"。
接下来,我们针对空单执行相似的逻辑:通过"rata_price"传入"ORDER_TYPE_SELL"计算"averageSellPrice";遍历全部持仓,将"trailingReference"赋值为"averageSellPrice"或POSITION_PRICE_OPEN, 当"SYMBOL_ASK相对"trailingReference"向下偏移达到"trailingStartPips * setPointValue",并且新止损价位低于原有止损或者原止损未设置时,将止损修改为"SYMBOL_ASK + (trailingStartPips - breakevenPips) * setPointValue"。最后,我们通过"PositionGetDouble(POSITION_TP)"读取原有止盈,确保修改止损时保留原先的止盈不变,并在上层函数中调用ChartRedraw刷新图表界面。编译后,我们得到以下结果:
在追踪止损前:

在追踪止损后:

现在,我们已经完成持仓管理逻辑,可以制作仪表盘,用于可视化展示各项账户指标数据。同样,我们将这部分功能封装为一个函数,便于后续维护管理。
//+------------------------------------------------------------------+ //| Display dashboard information | //+------------------------------------------------------------------+ void Display_Info() { buyCount = 0; //--- Reset buy count currentBuyLot = 0; //--- Reset current buy lot totalBuyLots = 0; //--- Reset total buy lots sellCount = 0; //--- Reset sell count currentSellLot = 0; //--- Reset current sell lot totalSellLots = 0; //--- Reset total sell lots totalSum = 0; //--- Reset total sum totalSwap = 0; //--- Reset total swap buyProfit = 0; //--- Reset buy profit sellProfit = 0; //--- Reset sell profit buyWeightedSum = 0; //--- Reset buy weighted sum sellWeightedSum = 0; //--- Reset sell weighted sum buyBreakEvenPrice = 0; //--- Reset buy breakeven price sellBreakEvenPrice = 0; //--- Reset sell breakeven price minBuyLot = 9999; //--- Initialize min buy lot minSellLot = 9999; //--- Initialize min sell lot maxSellPrice = 0; //--- Initialize max sell price minBuyPrice = 999999999; //--- Initialize min buy price for (int i = 0; i < PositionsTotal(); i++) { //--- Iterate through positions ulong ticket = PositionGetTicket(i); //--- Get position ticket if (ticket == 0) continue; //--- Skip invalid tickets if (PositionGetString(POSITION_SYMBOL) != Symbol()) continue; //--- Skip non-matching symbols if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY) { //--- Check buy position buyCount++; //--- Increment buy count totalOperations++; //--- Increment total operations currentBuyLot = PositionGetDouble(POSITION_VOLUME); //--- Set current buy lot buyProfit += PositionGetDouble(POSITION_PROFIT); //--- Add buy profit totalBuyLots += PositionGetDouble(POSITION_VOLUME); //--- Add to total buy lots minBuyLot = MathMin(minBuyLot, PositionGetDouble(POSITION_VOLUME)); //--- Update min buy lot buyWeightedSum += PositionGetDouble(POSITION_VOLUME) * PositionGetDouble(POSITION_PRICE_OPEN); //--- Add weighted open price minBuyPrice = MathMin(minBuyPrice, PositionGetDouble(POSITION_PRICE_OPEN)); //--- Update min buy price } if (PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL) { //--- Check sell position sellCount++; //--- Increment sell count totalOperations++; //--- Increment total operations currentSellLot = PositionGetDouble(POSITION_VOLUME); //--- Set current sell lot sellProfit += PositionGetDouble(POSITION_PROFIT); //--- Add sell profit totalSellLots += PositionGetDouble(POSITION_VOLUME); //--- Add to total sell lots minSellLot = MathMin(minSellLot, PositionGetDouble(POSITION_VOLUME)); //--- Update min sell lot sellWeightedSum += PositionGetDouble(POSITION_VOLUME) * PositionGetDouble(POSITION_PRICE_OPEN); //--- Add weighted open price maxSellPrice = MathMax(maxSellPrice, PositionGetDouble(POSITION_PRICE_OPEN)); //--- Update max sell price } } if (totalBuyLots > 0) { //--- Check buy lots buyBreakEvenPrice = buyWeightedSum / totalBuyLots; //--- Calculate buy breakeven } if (totalSellLots > 0) { //--- Check sell lots sellBreakEvenPrice = sellWeightedSum / totalSellLots; //--- Calculate sell breakeven } int minutesRemaining, secondsRemaining; //--- Declare time variables minutesRemaining = (int)(PeriodSeconds() - (TimeCurrent() - iTime(Symbol(), PERIOD_CURRENT, 0))); //--- Calculate remaining time secondsRemaining = minutesRemaining % 60; //--- Calculate seconds minutesRemaining = minutesRemaining / 60; //--- Calculate minutes long currentSpread = SymbolInfoInteger(Symbol(), SYMBOL_SPREAD); //--- Get current spread string spreadPrefix = "", minutesPrefix = "", secondsPrefix = ""; //--- Initialize prefixes if (currentSpread < 10) spreadPrefix = ".."; //--- Set spread prefix for single digit else if (currentSpread < 100) spreadPrefix = "."; //--- Set spread prefix for double digit if (minutesRemaining < 10) minutesPrefix = "0"; //--- Set minutes prefix if (secondsRemaining < 10) secondsPrefix = "0"; //--- Set seconds prefix int blinkingColorIndex; //--- Declare blinking color index color equityColor = clrGreen; //--- Initialize equity color if (AccountInfoDouble(ACCOUNT_EQUITY) - AccountInfoDouble(ACCOUNT_BALANCE) < 0.0) { //--- Check negative equity equityColor = clrRed; //--- Set equity color to red } color profitColor = (buyProfit + sellProfit >= 0) ? clrGreen : clrRed; //--- Set profit color MqlDateTime currentDateTime; //--- Declare datetime structure TimeToStruct(TimeCurrent(), currentDateTime); //--- Convert current time if (currentDateTime.sec >= 0 && currentDateTime.sec < 10) { //--- Check first 10 seconds blinkingColorIndex = clrRed; //--- Set red color } if (currentDateTime.sec >= 10 && currentDateTime.sec < 20) { //--- Check next 10 seconds blinkingColorIndex = clrOrange; //--- Set orange color } if (currentDateTime.sec >= 20 && currentDateTime.sec < 30) { //--- Check next 10 seconds blinkingColorIndex = clrBlue; //--- Set blue color } if (currentDateTime.sec >= 30 && currentDateTime.sec < 40) { //--- Check next 10 seconds blinkingColorIndex = clrDodgerBlue; //--- Set dodger blue color } if (currentDateTime.sec >= 40 && currentDateTime.sec < 50) { //--- Check next 10 seconds blinkingColorIndex = clrYellow; //--- Set yellow color } if (currentDateTime.sec >= 50 && currentDateTime.sec <= 59) { //--- Check last 10 seconds blinkingColorIndex = clrYellow; //--- Set yellow color } if (ObjectFind(0, "DashboardBG") < 0) { //--- Check dashboard background ObjectCreate(0, "DashboardBG", OBJ_RECTANGLE_LABEL, 0, 0, 0); //--- Create dashboard background ObjectSetInteger(0, "DashboardBG", OBJPROP_CORNER, 0); //--- Set corner ObjectSetInteger(0, "DashboardBG", OBJPROP_XDISTANCE, 100); //--- Set x distance ObjectSetInteger(0, "DashboardBG", OBJPROP_YDISTANCE, 20); //--- Set y distance ObjectSetInteger(0, "DashboardBG", OBJPROP_XSIZE, 260); //--- Set width ObjectSetInteger(0, "DashboardBG", OBJPROP_YSIZE, 300); //--- Set height ObjectSetInteger(0, "DashboardBG", OBJPROP_BGCOLOR, clrLightGray); //--- Set background color ObjectSetInteger(0, "DashboardBG", OBJPROP_BORDER_TYPE, BORDER_FLAT); //--- Set border type ObjectSetInteger(0, "DashboardBG", OBJPROP_COLOR, clrBlack); //--- Set border color ObjectSetInteger(0, "DashboardBG", OBJPROP_BACK, false); //--- Set to foreground } if (ObjectFind(0, "CLOSE ALL") < 0) { //--- Check close all button ObjectCreate(0, "CLOSE ALL", OBJ_BUTTON, 0, 0, 0); //--- Create close all button ObjectSetInteger(0, "CLOSE ALL", OBJPROP_CORNER, 0); //--- Set corner ObjectSetInteger(0, "CLOSE ALL", OBJPROP_XDISTANCE, 110); //--- Set x distance ObjectSetInteger(0, "CLOSE ALL", OBJPROP_YDISTANCE, 280); //--- Set y distance ObjectSetInteger(0, "CLOSE ALL", OBJPROP_XSIZE, 240); //--- Set width ObjectSetInteger(0, "CLOSE ALL", OBJPROP_YSIZE, 25); //--- Set height ObjectSetString(0, "CLOSE ALL", OBJPROP_TEXT, "Close All Positions"); //--- Set button text ObjectSetInteger(0, "CLOSE ALL", OBJPROP_COLOR, clrWhite); //--- Set text color ObjectSetInteger(0, "CLOSE ALL", OBJPROP_BGCOLOR, clrRed); //--- Set background color ObjectSetInteger(0, "CLOSE ALL", OBJPROP_BORDER_COLOR, clrBlack); //--- Set border color } string headerText = "Pin Bar Averaging EA"; //--- Set header text LABEL("Header", "Impact", 20, 110, 20, clrNavy, 0, headerText); //--- Create header label string copyrightText = "Copyright 2025, Allan Munene Mutiiria"; //--- Set copyright text LABEL("Copyright", "Arial", 9, 110, 55, clrBlack, 0, copyrightText); //--- Create copyright label string linkText = "https://t.me/Forex_Algo_Trader"; //--- Set link text LABEL("Link", "Arial", 9, 110, 70, clrBlue, 0, linkText); //--- Create link label string accountHeader = "Account Information"; //--- Set account header LABEL("AccountHeader", "Arial Bold", 10, 110, 90, clrBlack, 0, accountHeader); //--- Create account header label string balanceText = "Balance: " + DoubleToString(AccountInfoDouble(ACCOUNT_BALANCE), 2); //--- Set balance text LABEL("Balance", "Arial", 9, 120, 105, clrBlack, 0, balanceText); //--- Create balance label string equityText = "Equity: " + DoubleToString(AccountInfoDouble(ACCOUNT_EQUITY), 2); //--- Set equity text LABEL("Equity", "Arial", 9, 120, 120, equityColor, 0, equityText); //--- Create equity label string marginText = "Free Margin: " + DoubleToString(AccountInfoDouble(ACCOUNT_MARGIN_FREE), 2); //--- Set margin text LABEL("Margin", "Arial", 9, 120, 135, clrBlack, 0, marginText); //--- Create margin label string profitText = "Open Profit: " + DoubleToString(buyProfit + sellProfit, 2); //--- Set profit text LABEL("Profit", "Arial", 9, 120, 150, profitColor, 0, profitText); //--- Create profit label string positionsText = "Buy Positions: " + IntegerToString((int)buyCount) + " Sell Positions: " + IntegerToString((int)sellCount); //--- Set positions text LABEL("Positions", "Arial", 9, 120, 165, clrBlack, 0, positionsText); //--- Create positions label string buyBEText = "Buy Break Even: " + (buyCount > 0 ? DoubleToString(buyBreakEvenPrice, _Digits) : "-"); //--- Set buy breakeven text LABEL("BuyBE", "Arial", 9, 120, 180, clrBlack, 0, buyBEText); //--- Create buy breakeven label string sellBEText = "Sell Break Even: " + (sellCount > 0 ? DoubleToString(sellBreakEvenPrice, _Digits) : "-"); //--- Set sell breakeven text LABEL("SellBE", "Arial", 9, 120, 195, clrBlack, 0, sellBEText); //--- Create sell breakeven label string spreadText = "Spread: " + spreadPrefix + IntegerToString((int)currentSpread) + " points"; //--- Set spread text LABEL("Spread", "Arial", 9, 120, 210, clrBlack, 0, spreadText); //--- Create spread label string timeText = "Time to next bar: " + minutesPrefix + IntegerToString(minutesRemaining) + ":" + secondsPrefix + IntegerToString(secondsRemaining); //--- Set time text LABEL("Time", "Arial", 9, 120, 225, clrBlack, 0, timeText); //--- Create time label string pinbarText; //--- Declare pinbar text if (IsBuyPinbar()) pinbarText = "Buy Pinbar"; //--- Check buy pinbar else if (IsSellPinbar()) pinbarText = "Sell Pinbar"; //--- Check sell pinbar else pinbarText = "None"; //--- Set no pinbar LABEL("Pinbar", "Arial", 9, 120, 240, clrBlack, 0, "Pinbar Signal: " + pinbarText); //--- Create pinbar label string patternText = "Candle Pattern: " + CandleStick_Analyzer(); //--- Set candlestick pattern text LABEL("Pattern", "Arial", 9, 120, 255, clrBlack, 0, patternText); //--- Create pattern label }
我们实现"Display_Info"函数,用于生成完整的信息面板,实现交易状态的实时监控。首先,我们将"buyCount"等核心统计指标重置为各自的初始值,随后遍历全部持仓,筛选出与当前图表品种相匹配的多、空持仓,更新各项统计数据:累加持仓计数、汇总盈亏、交易量、加权开仓价格,记录最高与最低开仓价位,并在条件满足时计算盈亏平衡价。
接下来,我们使用PeriodSeconds获取当前周期每根K线的秒数,减去当前时间与iTime的时间差,以此计算距离下一根K线的剩余时间,换算得到"minutesRemaining"剩余分钟与"secondsRemaining"剩余秒数,同时为点差、时间显示设置格式化前缀。随后,我们确定"equityColor"(根据权益与余额的大小关系设置为绿色或红色)和"profitColor"(依据总盈亏设置为红色或绿色),再基于当前秒数设置闪烁颜色索引,实现界面闪烁视觉效果。最后,如果"DashboardBG"不存在,则使用ObjectCreate并选用OBJ_RECTANGLE_LABEL创建仪表盘背景,创建类型为"OBJ_BUTTON的"CLOSE ALL"按钮,再调用"LABEL"函数生成多个文本标签,分别展示 EA标题、版权、链接、账户信息(余额、权益、可用保证金、盈亏)、持仓数量、盈亏平衡价、点差、距离下一根K线倒计时、针形K线信号,以及由"CandleStick_Analyzer"输出的K线形态识别结果,实现交易信息清晰的动态可视化展示。对于按钮交互逻辑,我们在OnChartEvent事件处理器中实现。
//+------------------------------------------------------------------+ //| Handle chart events | //+------------------------------------------------------------------+ void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam) { if (id == CHARTEVENT_OBJECT_CLICK) { //--- Check object click event if (sparam == "CLOSE ALL") { //--- Check close all button ObjectSetInteger(0, "CLOSE ALL", OBJPROP_STATE, false); //--- Reset button state for (int positionIndex = PositionsTotal() - 1; positionIndex >= 0; positionIndex--) { //--- Iterate through positions ulong ticket = PositionGetTicket(positionIndex); //--- Get position ticket if (ticket == 0) continue; //--- Skip invalid tickets if (PositionGetString(POSITION_SYMBOL) == Symbol()) { //--- Check symbol obj_Trade.PositionClose(ticket); //--- Close position } } } } }
在OnChartEvent函数中,我们判断事件编号"id"是否等于CHARTEVENT_OBJECT_CLICK,以此捕获图表上绘图对象的鼠标点击事件。接下来,校验被点击对象的字符串参数"sparam"是否为"CLOSE ALL"按钮;如果匹配,则调用ObjectSetInteger,通过"OBJPROP_STATE"将按钮状态重置为未按下 (false)。随后遍历全部持仓,通过PositionGetTicket获取每一笔持仓票据,跳过无效票据,使用PositionGetString函数校验持仓品种是否与当前的"Symbol"一致。最后,对符合条件的持仓使用"obj_Trade.PositionClose"执行平仓,响应用户点击全部平仓按钮的手动操作指令,让仪表盘的“全部平仓”按钮可以便捷地手动管理当前持仓。在OnTick中调用函数并完成编译后,得到如下运行结果。

由效果图中可见,该EA能够识别并可视化绘制支撑位与阻力位、执行开仓以及均价加仓操作、对持仓启用移动止损,同时在面板上展示账户相关数据,至此我们完成了全部开发目标。接下来需完成的工作是程序回测,相关内容将在下一章节详细阐述。
回测
经过全面回测后,我们得到以下结果:
回测图:

回测报告:

结论
综上所述,我们已经在MQL5中完成针形K线均价加仓系统的开发。该系统利用针形K线形态作为开仓信号,通过均价加仓策略管理多笔持仓,并配套移动止损、盈亏平衡调整,以及用于实时监控的动态仪表盘。依靠"CandleStick_Analyzer"和"addAveragingOrder"等模块化函数组件,本程序为反转交易提供一套实现方案,同时风控参数全部支持自定义配置。
免责声明:本文仅用于教学目的。交易存在重大财务风险,市场剧烈波动可能导致资金损失。在实盘操作前,务必进行充分的历史回测,并建立严格的风险控制机制。
借助本文介绍的思路与代码实现,您可以根据自身交易风格对这套针形K线系统进行改造优化,进一步完善您的算法交易策略。祝您交易顺利!
本文由MetaQuotes Ltd译自英文
原文地址: https://www.mql5.com/en/articles/19087
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。
从基础到中级:对象事件(一)
新手在交易中的10个基本错误
MQL5 自动化交易策略(第二十五部分):基于最小二乘法拟合的趋势线交易 EA 与动态信号生成