HalfTrend 半趋势马丁交易系统
根据 HalfTrend 箭头信号开仓,10根K线高低点止损,浮亏小马丁3层加仓,动态移动止盈,含可视化面板
效果示意图 · AI 根据源码绘制,仅示意画了哪些元素,细节以实际运行为准
#property copyright "XAUOF 指标工坊"
#property version "1.00"
#property description "HalfTrend 信号 + 动态止损 + 小马丁加仓 + 移动止盈 + 可视化面板"
#include <Trade\Trade.mqh>
//+------------------------------------------------------------------+
//| 输入参数 |
//+------------------------------------------------------------------+
input group "===== 指标设置 ====="
input string InpIndicatorName = "HalfTrend"; // 指标文件名(不含.ex5后缀)
input int InpIndicatorAmplitude = 2; // 指标参数:灵敏度
input int InpIndicatorAtrPeriod = 100; // 指标参数:ATR周期
input int InpBuyBufferIndex = 6; // 买入箭头缓冲区序号(从0开始)
input int InpSellBufferIndex = 7; // 卖出箭头缓冲区序号(从0开始)
input group "===== 基础设置 ====="
input double InpLots = 0.01; // 首单手数
input int InpMagicNumber = 20261021; // EA 魔术号(用于识别自己的订单)
input string InpComment = "HT_Martin"; // 订单备注
input group "===== 止损设置 ====="
input int InpStopLookback = 10; // 计算止损用的K线根数(推荐10)
input int InpStopBuffer = 1000; // 止损缓冲点数(在高低点基础上再留的余量)
input group "===== 加仓设置 ====="
input bool InpEnableMartingale = true; // 是否启用马丁加仓
input int InpMartinLayers = 3; // 最大加仓层数(含首单共3单)
input double InpMartinMultiplier = 1.2; // 加仓手数倍数(每层手数 × 1.2)
input int InpMartinDistance = 600; // 加仓距离(浮亏多少点后加仓)
input group "===== 移动止盈设置 ====="
input bool InpEnableTrailing = true; // 是否启用移动止盈
input int InpTrailingStart = 300; // 启动移动止盈的盈利点数(价格盈利超过此值后开始跟踪)
input int InpTrailingStep = 150; // 移动止盈跟踪距离(止损与价格保持的距离)
input group "===== 风控设置 ====="
input int InpMaxSpread = 300; // 最大允许点差(超过此值不开仓)
input int InpMaxSlippage = 50; // 最大滑点(点数)
input group "===== 可视化面板 ====="
input bool InpShowPanel = true; // 是否显示可视化面板
input int InpPanelX = 20; // 面板X坐标(像素)
input int InpPanelY = 50; // 面板Y坐标(像素)
input color InpPanelColor = clrWhite; // 面板文字颜色
input int InpPanelFontSize = 9; // 面板字体大小
//+------------------------------------------------------------------+
//| 全局变量 |
//+------------------------------------------------------------------+
CTrade g_trade; // 交易对象
int g_indicatorHandle = INVALID_HANDLE; // 指标句柄
// 持仓状态跟踪
struct PositionState
{
bool hasPosition; // 是否有持仓
int direction; // 0=无, 1=多, -1=空
int layers; // 已开仓层数(1、2、3)
double firstPrice; // 首单开仓价
double stopLoss; // 当前止损价
double totalLots; // 总手数
double highestPrice; // 多单:持仓后的最高价;用于移动止盈
double lowestPrice; // 空单:持仓后的最低价;用于移动止盈
bool layerAdded[10]; // 每层是否已加仓(防止重复加仓)
};
PositionState g_position;
// 每日统计
struct DailyStats
{
datetime lastResetDate; // 上次重置日期
int todayOrders; // 当日开单数
double todayProfit; // 当日盈亏(金额)
double maxDrawdown; // 最大回撤
};
DailyStats g_stats;
datetime g_lastSignalTime = 0; // 上次信号时间(防止同一根K线重复开仓)
string g_panelPrefix = "HTPanel_"; // 面板对象名前缀
//+------------------------------------------------------------------+
//| 初始化 |
//+------------------------------------------------------------------+
int OnInit()
{
//--- 参数校验
if(InpLots <= 0)
{
Print("[错误] 首单手数必须大于 0");
return INIT_PARAMETERS_INCORRECT;
}
if(InpStopLookback < 1)
{
Print("[错误] 止损回溯K线数必须大于等于 1");
return INIT_PARAMETERS_INCORRECT;
}
if(InpMartinLayers < 1 || InpMartinLayers > 10)
{
Print("[错误] 加仓层数必须在 1-10 之间");
return INIT_PARAMETERS_INCORRECT;
}
if(InpMartinMultiplier < 1.0)
{
Print("[错误] 加仓倍数必须大于等于 1.0");
return INIT_PARAMETERS_INCORRECT;
}
//--- 设置 CTrade
g_trade.SetExpertMagicNumber(InpMagicNumber);
g_trade.SetDeviationInPoints(InpMaxSlippage);
g_trade.SetTypeFilling(ORDER_FILLING_FOK);
if(!g_trade.SetTypeFillingBySymbol(_Symbol))
g_trade.SetTypeFilling(ORDER_FILLING_IOC);
//--- 创建指标句柄
g_indicatorHandle = iCustom(_Symbol, PERIOD_CURRENT, InpIndicatorName,
InpIndicatorAmplitude,
InpIndicatorAtrPeriod,
true, // InpShowBands
true, // InpShowArrows
false, // InpAlertsOn
false, false, false, false);
if(g_indicatorHandle == INVALID_HANDLE)
{
Print("[错误] 无法创建指标句柄,请确认指标文件名: ", InpIndicatorName, ".ex5,错误代码 ", GetLastError());
return INIT_FAILED;
}
//--- 初始化持仓状态和统计
ResetPositionState();
ResetDailyStats();
//--- 创建可视化面板
if(InpShowPanel)
CreatePanel();
//--- 扫描现有持仓,恢复状态(断线重启时)
RestorePositionState();
//--- 打印所有参数到日志
Print("========== HalfTrend 马丁 EA 启动 ==========");
Print("[参数] 指标名称: ", InpIndicatorName, ",买入缓冲区: ", InpBuyBufferIndex, ",卖出缓冲区: ", InpSellBufferIndex);
Print("[参数] 首单手数: ", InpLots);
Print("[参数] 魔术号: ", InpMagicNumber);
Print("[参数] 止损回溯K线: ", InpStopLookback, " 根,缓冲点数: ", InpStopBuffer);
Print("[参数] 马丁加仓: ", (InpEnableMartingale ? "启用" : "禁用"), ",最大层数: ", InpMartinLayers, ",倍数: ", InpMartinMultiplier, ",距离: ", InpMartinDistance, " 点");
Print("[参数] 移动止盈: ", (InpEnableTrailing ? "启用" : "禁用"), ",启动: ", InpTrailingStart, " 点,跟踪: ", InpTrailingStep, " 点");
Print("[参数] 最大点差: ", InpMaxSpread, " 点,最大滑点: ", InpMaxSlippage, " 点");
Print("============================================");
return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
//| 卸载 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
if(g_indicatorHandle != INVALID_HANDLE)
IndicatorRelease(g_indicatorHandle);
if(InpShowPanel)
DeletePanel();
Print("[卸载] HalfTrend 马丁 EA 已停止,原因代码: ", reason);
}
//+------------------------------------------------------------------+
//| 主循环 - 修复:加仓检查移到这里,每个 Tick 都执行 |
//+------------------------------------------------------------------+
void OnTick()
{
//--- 检查是否需要重置每日统计(跨天)
CheckDailyReset();
//--- 更新当前持仓状态
UpdatePositionState();
//--- 持续监控:移动止盈
if(InpEnableTrailing && g_position.hasPosition)
ManageTrailingStop();
//--- 持续监控:马丁加仓(关键修复:每个 Tick 都检查)
if(InpEnableMartingale && g_position.hasPosition && g_position.layers < InpMartinLayers)
CheckAndAddPosition();
//--- 检查新信号
int signal = GetSignal();
if(signal == 1) // 买入信号
{
HandleBuySignal();
}
else if(signal == -1) // 卖出信号
{
HandleSellSignal();
}
//--- 更新可视化面板
if(InpShowPanel)
UpdatePanel();
}
//+------------------------------------------------------------------+
//| 获取指标信号 |
//| 返回:1=买入箭头, -1=卖出箭头, 0=无信号 |
//+------------------------------------------------------------------+
int GetSignal()
{
if(BarsCalculated(g_indicatorHandle) < 2)
return 0;
double arrUp[2], arrDn[2];
if(CopyBuffer(g_indicatorHandle, InpBuyBufferIndex, 0, 2, arrUp) < 2) return 0;
if(CopyBuffer(g_indicatorHandle, InpSellBufferIndex, 0, 2, arrDn) < 2) return 0;
//--- 读取倒数第二根已收盘 K 线的箭头(索引 0)
datetime barTime = iTime(_Symbol, PERIOD_CURRENT, 1);
if(arrUp[0] != EMPTY_VALUE && arrUp[0] > 0 && g_lastSignalTime != barTime)
{
g_lastSignalTime = barTime;
return 1;
}
if(arrDn[0] != EMPTY_VALUE && arrDn[0] > 0 && g_lastSignalTime != barTime)
{
g_lastSignalTime = barTime;
return -1;
}
return 0;
}
//+------------------------------------------------------------------+
//| 处理买入信号 |
//+------------------------------------------------------------------+
void HandleBuySignal()
{
//--- 如果当前有空单,先全部平掉
if(g_position.direction == -1)
{
Print("[信号] 检测到买入信号,平掉所有空单");
CloseAllPositions();
ResetPositionState();
}
//--- 如果已有多单,不重复开仓(加仓由 CheckAndAddPosition 负责)
if(g_position.direction == 1)
{
Print("[跳过] 已有多单,等待加仓逻辑触发");
return;
}
//--- 无持仓,开首单
OpenFirstPosition(1);
}
//+------------------------------------------------------------------+
//| 处理卖出信号 |
//+------------------------------------------------------------------+
void HandleSellSignal()
{
//--- 如果当前有多单,先全部平掉
if(g_position.direction == 1)
{
Print("[信号] 检测到卖出信号,平掉所有多单");
CloseAllPositions();
ResetPositionState();
}
//--- 如果已有空单,不重复开仓
if(g_position.direction == -1)
{
Print("[跳过] 已有空单,等待加仓逻辑触发");
return;
}
//--- 无持仓,开首单
OpenFirstPosition(-1);
}
//+------------------------------------------------------------------+
//| 开首单 |
//+------------------------------------------------------------------+
void OpenFirstPosition(int direction)
{
//--- 检查点差
double spread = SymbolInfoInteger(_Symbol, SYMBOL_SPREAD);
if(spread > InpMaxSpread)
{
Print("[跳过] 当前点差 ", (int)spread, " 点,超过限制 ", InpMaxSpread, " 点");
return;
}
//--- 检查交易是否允许
if(!SymbolInfoInteger(_Symbol, SYMBOL_TRADE_MODE))
{
Print("[跳过] 交易未启用");
return;
}
//--- 规整手数
double lots = NormalizeLots(InpLots);
if(lots <= 0)
{
Print("[错误] 手数规整后为 0,无法开仓");
return;
}
//--- 计算止损价
double sl = CalculateStopLoss(direction);
if(sl <= 0)
{
Print("[错误] 止损价计算失败");
return;
}
//--- 获取当前价格
double price = (direction == 1) ? SymbolInfoDouble(_Symbol, SYMBOL_ASK) : SymbolInfoDouble(_Symbol, SYMBOL_BID);
//--- 检查止损距离是否满足最小要求
int stopsLevel = (int)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
double minDistance = stopsLevel * _Point;
if(MathAbs(price - sl) < minDistance)
{
Print("[警告] 止损距离 ", (int)(MathAbs(price - sl) / _Point), " 点小于最小距离 ", stopsLevel, " 点,调整止损");
if(direction == 1)
sl = price - minDistance - 10 * _Point;
else
sl = price + minDistance + 10 * _Point;
}
//--- 下单
bool result = false;
if(direction == 1)
result = g_trade.Buy(lots, _Symbol, 0, sl, 0, InpComment);
else
result = g_trade.Sell(lots, _Symbol, 0, sl, 0, InpComment);
if(result)
{
g_position.hasPosition = true;
g_position.direction = direction;
g_position.layers = 1;
g_position.firstPrice = price;
g_position.stopLoss = sl;
g_position.totalLots = lots;
g_position.highestPrice = price;
g_position.lowestPrice = price;
ArrayInitialize(g_position.layerAdded, false);
g_position.layerAdded[0] = true;
g_stats.todayOrders++;
Print("[开仓] ", (direction == 1 ? "做多" : "做空"), " ", lots, " 手,价格: ", price,
",止损: ", sl, ",止损距离: ", (int)(MathAbs(price - sl) / _Point), " 点");
}
else
{
Print("[失败] 开仓失败,错误代码: ", g_trade.ResultRetcode(), ",错误描述: ", g_trade.ResultRetcodeDescription());
}
}
//+------------------------------------------------------------------+
//| 检查并加仓 - 修复:移除 direction 参数,直接读取 g_position |
//+------------------------------------------------------------------+
void CheckAndAddPosition()
{
if(!g_position.hasPosition)
return;
int direction = g_position.direction;
//--- 计算当前浮动盈亏(点数)
double currentPrice = (direction == 1) ? SymbolInfoDouble(_Symbol, SYMBOL_BID) : SymbolInfoDouble(_Symbol, SYMBOL_ASK);
double floatingPips = 0;
if(direction == 1)
floatingPips = (currentPrice - g_position.firstPrice) / _Point;
else
floatingPips = (g_position.firstPrice - currentPrice) / _Point;
//--- 计算应该加到第几层
int targetLayer = 1;
double lossThreshold = -InpMartinDistance;
while(targetLayer < InpMartinLayers && floatingPips < lossThreshold)
{
targetLayer++;
lossThreshold -= InpMartinDistance;
}
//--- 如果应加的层还没加过
if(targetLayer > g_position.layers && !g_position.layerAdded[targetLayer - 1])
{
//--- 检查点差
double spread = SymbolInfoInteger(_Symbol, SYMBOL_SPREAD);
if(spread > InpMaxSpread)
{
Print("[跳过] 加仓:当前点差 ", (int)spread, " 点,超过限制");
return;
}
//--- 计算加仓手数
double newLots = InpLots;
for(int i = 1; i < targetLayer; i++)
newLots *= InpMartinMultiplier;
newLots = NormalizeLots(newLots);
if(newLots <= 0)
{
Print("[错误] 加仓手数规整后为 0");
return;
}
//--- 加仓单使用相同的止损
bool result = false;
if(direction == 1)
result = g_trade.Buy(newLots, _Symbol, 0, g_position.stopLoss, 0, InpComment);
else
result = g_trade.Sell(newLots, _Symbol, 0, g_position.stopLoss, 0, InpComment);
if(result)
{
g_position.layers = targetLayer;
g_position.totalLots += newLots;
g_position.layerAdded[targetLayer - 1] = true;
g_stats.todayOrders++;
Print("[加仓] 第 ", targetLayer, " 层,", (direction == 1 ? "做多" : "做空"), " ", newLots,
" 手,浮亏: ", (int)floatingPips, " 点,总手数: ", g_position.totalLots);
}
else
{
Print("[失败] 加仓失败,错误代码: ", g_trade.ResultRetcode(), ",描述: ", g_trade.ResultRetcodeDescription());
}
}
}
//+------------------------------------------------------------------+
//| 管理移动止盈 |
//+------------------------------------------------------------------+
void ManageTrailingStop()
{
double currentPrice = (g_position.direction == 1) ? SymbolInfoDouble(_Symbol, SYMBOL_BID) : SymbolInfoDouble(_Symbol, SYMBOL_ASK);
if(g_position.direction == 1) // 多单
{
//--- 更新最高价
if(currentPrice > g_position.highestPrice)
g_position.highestPrice = currentPrice;
//--- 盈利超过启动距离
double profit = (currentPrice - g_position.firstPrice) / _Point;
if(profit > InpTrailingStart)
{
double newSL = currentPrice - InpTrailingStep * _Point;
//--- 止损只能上移
if(newSL > g_position.stopLoss + _Point)
{
ModifyAllPositionsSL(newSL);
}
}
}
else if(g_position.direction == -1) // 空单
{
//--- 更新最低价
if(currentPrice < g_position.lowestPrice)
g_position.lowestPrice = currentPrice;
//--- 盈利超过启动距离
double profit = (g_position.firstPrice - currentPrice) / _Point;
if(profit > InpTrailingStart)
{
double newSL = currentPrice + InpTrailingStep * _Point;
//--- 止损只能下移
if(newSL < g_position.stopLoss - _Point)
{
ModifyAllPositionsSL(newSL);
}
}
}
}
//+------------------------------------------------------------------+
//| 修改所有同方向持仓的止损 |
//+------------------------------------------------------------------+
void ModifyAllPositionsSL(double newSL)
{
int modifiedCount = 0;
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong ticket = PositionGetTicket(i);
if(ticket == 0) continue;
if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
if(PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) continue;
double currentSL = PositionGetDouble(POSITION_SL);
double tp = PositionGetDouble(POSITION_TP);
if(MathAbs(newSL - currentSL) > _Point)
{
if(g_trade.PositionModify(ticket, newSL, tp))
modifiedCount++;
}
}
if(modifiedCount > 0)
{
Print("[移动止盈] 已修改 ", modifiedCount, " 个订单,新止损: ", newSL);
g_position.stopLoss = newSL;
}
}
//+------------------------------------------------------------------+
//| 计算止损价(近N根K线高低点 ± 缓冲) |
//+------------------------------------------------------------------+
double CalculateStopLoss(int direction)
{
double highArray[], lowArray[];
ArraySetAsSeries(highArray, true);
ArraySetAsSeries(lowArray, true);
int copied = CopyHigh(_Symbol, PERIOD_CURRENT, 0, InpStopLookback + 1, highArray);
if(copied < InpStopLookback + 1) return 0;
copied = CopyLow(_Symbol, PERIOD_CURRENT, 0, InpStopLookback + 1, lowArray);
if(copied < InpStopLookback + 1) return 0;
double highest = highArray[ArrayMaximum(highArray, 0, InpStopLookback)];
double lowest = lowArray[ArrayMinimum(lowArray, 0, InpStopLookback)];
double sl = 0;
if(direction == 1)
sl = lowest - InpStopBuffer * _Point;
else
sl = highest + InpStopBuffer * _Point;
return NormalizeDouble(sl, _Digits);
}
//+------------------------------------------------------------------+
//| 规整手数 |
//+------------------------------------------------------------------+
double NormalizeLots(double lots)
{
double minLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
double maxLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
double stepLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
if(lots < minLot) lots = minLot;
if(lots > maxLot) lots = maxLot;
lots = MathFloor(lots / stepLot) * stepLot;
return NormalizeDouble(lots, 2);
}
//+------------------------------------------------------------------+
//| 平掉所有本 EA 的持仓 |
//+------------------------------------------------------------------+
void CloseAllPositions()
{
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
ulong ticket = PositionGetTicket(i);
if(ticket == 0) continue;
if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
if(PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) continue;
double profit = PositionGetDouble(POSITION_PROFIT);
g_stats.todayProfit += profit;
if(g_trade.PositionClose(ticket))
Print("[平仓] 订单 ", ticket, " 已平仓,盈亏: ", profit);
else
Print("[失败] 平仓订单 ", ticket, " 失败,错误代码: ", g_trade.ResultRetcode());
}
}
//+------------------------------------------------------------------+
//| 更新持仓状态 - 修复:恢复 firstPrice |
//+------------------------------------------------------------------+
void UpdatePositionState()
{
int buyCount = 0, sellCount = 0;
double totalBuyLots = 0, totalSellLots = 0;
double firstBuyPrice = 0, firstSellPrice = 0;
double buySL = 0, sellSL = 0;
for(int i = 0; i < PositionsTotal(); i++)
{
ulong ticket = PositionGetTicket(i);
if(ticket == 0) continue;
if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
if(PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) continue;
long type = PositionGetInteger(POSITION_TYPE);
double lots = PositionGetDouble(POSITION_VOLUME);
double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);
double sl = PositionGetDouble(POSITION_SL);
datetime openTime = (datetime)PositionGetInteger(POSITION_TIME);
if(type == POSITION_TYPE_BUY)
{
buyCount++;
totalBuyLots += lots;
//--- 首单价格:取最早开仓的那一单
if(firstBuyPrice == 0 || openTime < (datetime)PositionGetInteger(POSITION_TIME))
{
firstBuyPrice = openPrice;
buySL = sl;
}
}
else if(type == POSITION_TYPE_SELL)
{
sellCount++;
totalSellLots += lots;
if(firstSellPrice == 0 || openTime < (datetime)PositionGetInteger(POSITION_TIME))
{
firstSellPrice = openPrice;
sellSL = sl;
}
}
}
//--- 如果没有持仓,重置状态
if(buyCount == 0 && sellCount == 0)
{
if(g_position.hasPosition)
{
Print("[状态] 所有持仓已平仓");
ResetPositionState();
}
return;
}
//--- 更新状态
if(buyCount > 0)
{
g_position.hasPosition = true;
g_position.direction = 1;
g_position.layers = buyCount;
g_position.totalLots = totalBuyLots;
//--- 如果 firstPrice 还是0(说明刚启动EA),就从持仓恢复
if(g_position.firstPrice == 0)
g_position.firstPrice = firstBuyPrice;
if(g_position.stopLoss == 0)
g_position.stopLoss = buySL;
}
else if(sellCount > 0)
{
g_position.hasPosition = true;
g_position.direction = -1;
g_position.layers = sellCount;
g_position.totalLots = totalSellLots;
if(g_position.firstPrice == 0)
g_position.firstPrice = firstSellPrice;
if(g_position.stopLoss == 0)
g_position.stopLoss = sellSL;
}
}
//+------------------------------------------------------------------+
//| 恢复持仓状态(断线重启时) |
//+------------------------------------------------------------------+
void RestorePositionState()
{
UpdatePositionState();
if(g_position.hasPosition)
{
Print("[恢复] 检测到现有持仓:方向=", (g_position.direction == 1 ? "多" : "空"),
",层数=", g_position.layers, ",首单价=", g_position.firstPrice,
",止损=", g_position.stopLoss, ",总手数=", g_position.totalLots);
//--- 标记已有的层数为"已加仓"
for(int i = 0; i < g_position.layers; i++)
g_position.layerAdded[i] = true;
}
}
//+------------------------------------------------------------------+
//| 重置持仓状态 |
//+------------------------------------------------------------------+
void ResetPositionState()
{
g_position.hasPosition = false;
g_position.direction = 0;
g_position.layers = 0;
g_position.firstPrice = 0;
g_position.stopLoss = 0;
g_position.totalLots = 0;
g_position.highestPrice = 0;
g_position.lowestPrice = 0;
ArrayInitialize(g_position.layerAdded, false);
}
//+------------------------------------------------------------------+
//| 检查每日重置 |
//+------------------------------------------------------------------+
void CheckDailyReset()
{
MqlDateTime dt;
TimeToStruct(TimeCurrent(), dt);
datetime today = StringToTime(IntegerToString(dt.year) + "." + IntegerToString(dt.mon) + "." + IntegerToString(dt.day));
if(g_stats.lastResetDate != today)
{
if(g_stats.lastResetDate != 0)
Print("[统计] 昨日盈亏: ", g_stats.todayProfit, " USD,开单数: ", g_stats.todayOrders);
ResetDailyStats();
g_stats.lastResetDate = today;
}
}
//+------------------------------------------------------------------+
//| 重置每日统计 |
//+------------------------------------------------------------------+
void ResetDailyStats()
{
MqlDateTime dt;
TimeToStruct(TimeCurrent(), dt);
g_stats.lastResetDate = StringToTime(IntegerToString(dt.year) + "." + IntegerToString(dt.mon) + "." + IntegerToString(dt.day));
g_stats.todayOrders = 0;
g_stats.todayProfit = 0;
g_stats.maxDrawdown = 0;
}
//+------------------------------------------------------------------+
//| 创建可视化面板 |
//+------------------------------------------------------------------+
void CreatePanel()
{
string labels[] = {"标题", "状态", "层数", "手数", "浮盈", "今日盈亏", "今日单数"};
int count = ArraySize(labels);
for(int i = 0; i < count; i++)
{
string objName = g_panelPrefix + IntegerToString(i);
if(ObjectCreate(0, objName, OBJ_LABEL, 0, 0, 0))
{
ObjectSetInteger(0, objName, OBJPROP_CORNER, CORNER_LEFT_UPPER);
ObjectSetInteger(0, objName, OBJPROP_XDISTANCE, InpPanelX);
ObjectSetInteger(0, objName, OBJPROP_YDISTANCE, InpPanelY + i * (InpPanelFontSize + 5));
ObjectSetInteger(0, objName, OBJPROP_COLOR, InpPanelColor);
ObjectSetInteger(0, objName, OBJPROP_FONTSIZE, InpPanelFontSize);
ObjectSetString(0, objName, OBJPROP_FONT, "Arial");
ObjectSetString(0, objName, OBJPROP_TEXT, labels[i]);
}
}
}
//+------------------------------------------------------------------+
//| 更新可视化面板 |
//+------------------------------------------------------------------+
void UpdatePanel()
{
string status = "无持仓";
string layers = "-";
string lots = "-";
string floating = "-";
if(g_position.hasPosition)
{
status = (g_position.direction == 1) ? "多单" : "空单";
layers = IntegerToString(g_position.layers) + " / " + IntegerToString(InpMartinLayers);
lots = DoubleToString(g_position.totalLots, 2);
//--- 计算浮动盈亏
double floatingProfit = 0;
for(int i = 0; i < PositionsTotal(); i++)
{
ulong ticket = PositionGetTicket(i);
if(ticket == 0) continue;
if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
if(PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) continue;
floatingProfit += PositionGetDouble(POSITION_PROFIT);
}
floating = DoubleToString(floatingProfit, 2) + " USD";
}
ObjectSetString(0, g_panelPrefix + "0", OBJPROP_TEXT, "=== HalfTrend 马丁 EA ===");
ObjectSetString(0, g_panelPrefix + "1", OBJPROP_TEXT, "状态: " + status);
ObjectSetString(0, g_panelPrefix + "2", OBJPROP_TEXT, "层数: " + layers);
ObjectSetString(0, g_panelPrefix + "3", OBJPROP_TEXT, "手数: " + lots);
ObjectSetString(0, g_panelPrefix + "4", OBJPROP_TEXT, "浮盈: " + floating);
ObjectSetString(0, g_panelPrefix + "5", OBJPROP_TEXT, "今日盈亏: " + DoubleToString(g_stats.todayProfit, 2) + " USD");
ObjectSetString(0, g_panelPrefix + "6", OBJPROP_TEXT, "今日开单: " + IntegerToString(g_stats.todayOrders) + " 次");
}
//+------------------------------------------------------------------+
//| 删除可视化面板 |
//+------------------------------------------------------------------+
void DeletePanel()
{
for(int i = 0; i < 10; i++)
{
string objName = g_panelPrefix + IntegerToString(i);
ObjectDelete(0, objName);
}
}
//+------------------------------------------------------------------+作品说明
你说得对,我发现了问题:加仓检查只在有新箭头信号时才执行,但马丁加仓应该每个 Tick 都持续监控浮亏。
另外上次代码被截断了,我现在给你完整修复版:
使用说明
- 安装:将指标 HalfTrend.ex5 和本 EA 都放入对应目录,重启 MT5 后挂载到图表上
- 关键修复:
- 马丁加仓现在每个 Tick 都检查浮亏,不再依赖新信号触发
UpdatePositionState()正确恢复首单价格,断线重启也能继续管理- 增加
layerAdded数组防止重复加仓
- 参数调整:
- 指标名称:如果你的指标文件名不是 HalfTrend.ex5,修改"指标文件名"参数
- 缓冲区序号:默认 6 是买入箭头、7 是卖出箭头,如果指标不同需调整
- 马丁距离:600 点表示浮亏 600 点后加第二层,再亏 600 点加第三层
- 测试建议:先在模拟盘小手数测试,观察"专家"日志确认加仓逻辑是否触发
- 面板显示:左上角显示当前状态、层数、浮盈、今日盈亏等,方便监控
- 止损保护:所有加仓单共用首单的止损位,不会因加仓而扩大风险暴露
AI 解读根据源码逐行分析写成,供参考,以实际运行为准
这是什么
这是一个基于 HalfTrend 指标箭头信号的全自动交易 EA,专为 MT5 设计。它在指标发出买卖箭头时开仓,用近 10 根 K 线的高低点计算止损位,持仓浮亏时按马丁策略加仓最多 3 层,盈利后自动跟踪止盈。适合想用趋势指标信号 + 加仓策略提高盈亏比的交易者。
逻辑原理
信号来源:EA 调用自定义指标 HalfTrend.ex5(需单独安装),读取缓冲区 6 的买入箭头和缓冲区 7 的卖出箭头。检测到新箭头时(倒数第二根已收盘 K 线),如果当前无持仓或持有反向仓位就执行开仓或平仓转向。
止损计算:开首单前,EA 读取最近 10 根 K 线的最高点和最低点。做多时止损 = 最低点 - 1000 点缓冲,做空时止损 = 最高点 + 1000 点缓冲。所有加仓单共用首单的这个止损位,不会因加仓而扩大风险暴露。
马丁加仓:每个 Tick 检查浮动盈亏。首单后,如果浮亏超过 600 点触发第二层加仓,再亏 600 点触发第三层。每层手数 = 上一层手数 × 1.2 倍。用 layerAdded 数组防止同一层重复加仓。加仓单与首单方向相同,使用相同止损价。
移动止盈:持仓盈利超过 300 点(价格盈利,非金额)后启动。多单时,止损跟随价格保持 150 点距离,且只能向上移动;空单反向。移动止盈会修改所有同方向持仓的止损,实现部分利润保护。
信号转向:检测到反向箭头时,EA 先平掉所有当前持仓(含加仓单),统计盈亏,然后按新方向开首单。
参数说明
指标设置
- 指标文件名(HalfTrend):自定义指标的文件名,不含 .ex5 后缀,需与 Indicators 文件夹里的文件名一致
- 指标参数:灵敏度(2):传给 HalfTrend 的 Amplitude 参数,值越小越灵敏
- 指标参数:ATR 周期(100):HalfTrend 用来计算波动的 ATR 周期数
- 买入箭头缓冲区序号(6):HalfTrend 指标的买入箭头缓冲区索引,默认 6
- 卖出箭头缓冲区序号(7):卖出箭头缓冲区索引,默认 7
基础设置
- 首单手数(0.01):第一次开仓的手数,后续加仓会乘以倍数
- EA 魔术号(20261021):用于识别 EA 自己的订单,不要与其他 EA 重复
- 订单备注(HT_Martin):所有订单的注释字段
止损设置
- 计算止损用的 K 线根数(10):向前回溯多少根 K 线找高低点,10 根约等于看近期波动
- 止损缓冲点数(1000):在高低点基础上再留的安全距离,1000 点 = 100 点(五位报价)
加仓设置
- 是否启用马丁加仓(true):关闭后只开首单,不加仓
- 最大加仓层数(3):含首单最多 3 单,即首单 + 2 次加仓
- 加仓手数倍数(1.2):每层手数是上一层的 1.2 倍,首单 0.01 则二层 0.012、三层 0.0144
- 加仓距离(600):浮亏多少点后触发加仓,600 点 = 60 点(五位报价)
移动止盈设置
- 是否启用移动止盈(true):关闭后止损固定不移动
- 启动移动止盈的盈利点数(300):价格盈利超过 300 点(30 点五位)后开始跟踪
- 移动止盈跟踪距离(150):止损与当前价格保持的距离,150 点 = 15 点
风控设置
- 最大允许点差(300):点差超过此值不开仓,防止高点差时段交易
- 最大滑点(50):下单时允许的最大滑点,超过会拒单
可视化面板
- 是否显示可视化面板(true):在图表左上角显示 EA 状态、层数、浮盈等信息
- 其余为面板位置和样式参数
怎么用
- 安装指标:先把 HalfTrend.ex5 指标文件放到 MT5 的
MQL5\Indicators文件夹,重启 MT5 - 安装 EA:将本 EA 文件放到
MQL5\Experts文件夹,刷新导航器 - 挂载图表:把 EA 拖到任意品种任意周期的图表上(推荐 H1 或 H4),勾选"允许自动交易"
- 参数调整:首次使用建议把首单手数改成 0.01,马丁距离根据品种波动调整(黄金可用 600-1000 点,货币对可用 300-500 点)
- 观察信号:EA 会在图表左上角显示面板,"状态"显示当前持仓方向,"层数"显示已开几单
- 查看日志:打开"专家"选项卡,EA 会输出每次开仓、加仓、平仓、移动止盈的详细信息
- 监控持仓:在"交易"窗口可以看到所有订单,备注都是"HT_Martin",止损价相同
适用场景
适合的品种:XAUUSD(黄金)、EURUSD、GBPUSD 等波动较大的主流品种,因为需要足够的趋势空间让移动止盈发挥作用。
适合的周期:H1(1 小时)或 H4(4 小时)。太小的周期(M5、M15)信号频繁且噪音多,容易假突破;太大的周期(D1)信号太少且止损过大。
适合的行情:单边趋势行情,HalfTrend 指标在趋势中表现较好,箭头出现后价格持续朝信号方向走,移动止盈能锁住利润。震荡行情中指标会频繁转向,每次转向都平仓重开,可能反复止损。
不适合的场景:
- 窄幅震荡:价格在小区间来回扫,止损容易被打掉,加仓也救不回来
- 重大新闻前后:点差暴涨、滑点严重,EA 会拒绝开仓但已有持仓可能被快速止损
- 低流动性时段:亚洲盘尾盘或节假日,点差大且容易假突破
风险提示
马丁加仓风险:浮亏时加仓会放大仓位,如果行情持续逆势,3 层加仓的总手数是首单的 3.64 倍(1 + 1.2 + 1.44)。止损一旦触发,亏损金额比单笔首单大得多。小账户或大手数慎用。
指标滞后:HalfTrend 是趋势跟踪指标,箭头出现时价格通常已经走了一段,属于追涨杀跌类型。如果趋势很快反转,可能刚开仓就浮亏触发加仓。
止损距离固定:首单止损用近 10 根 K 线高低点计算,行情剧烈波动时 10 根可能覆盖不够,止损可能过近;平静行情时止损又可能过远。需根据品种和周期测试调整 InpStopLookback 和 InpStopBuffer。
移动止盈门槛:必须盈利 300 点(默认参数)才启动跟踪,如果行情只给 200 点利润就反转,移动止盈根本没机会工作,最终可能被固定止损打掉或回吐利润。
信号转向清仓:一旦出现反向箭头,EA 立即平掉所有持仓(含加仓单)。如果指标在震荡中反复变向,会导致频繁开平仓,手续费和滑点累积。
无重绘检测:代码未判断 HalfTrend 指标是否重绘。如果指标在新 K 线形成后修改历史箭头,EA 的信号逻辑会失效(不过 EA 读取的是倒数第二根已收盘 K 线,一定程度避免了当前柱重绘)。
点差和滑点:EA 只在点差超过 300 点时拒绝开仓,但加仓时也检查点差。实际成交价可能因滑点偏离预期,尤其是快速行情中的加仓单。
常见问题
- 这个 EA 会重绘吗?
- EA 本身不重绘,它只在每根 K 线收盘后读取指标箭头。但如果 HalfTrend 指标本身会重绘历史箭头,可能影响回测结果的准确性。实盘中 EA 读取的是已收盘 K 线,重绘影响较小。
- 马丁加仓最多亏多少?
- 默认参数下,3 层加仓总手数是首单的 3.64 倍。如果首单 0.01 手,止损 1000 点(100 点五位报价),最大亏损约 = 0.01 × 3.64 × 100 × 每点价值。黄金每点 1 美元时约 364 美元,具体看品种合约规格。
- 可以用在 XAUUSD 以外的品种吗?
- 可以。代码没有品种限制,但参数需调整。货币对波动比黄金小,马丁距离(InpMartinDistance)建议改成 300-500 点,止损缓冲(InpStopBuffer)改成 500-800 点。原油、指数等也能用,需自行回测优化参数。
- MT4 能用吗?
- 不能。这是 MQL5 代码,只能在 MT5 平台运行。MT4 需要用 MQL4 重写,且 MT4 的 CTrade 类和指标调用方式不同。
- 参数默认值适合什么周期?
- 默认参数(马丁距离 600 点、止损回溯 10 根、移动止盈 300 点启动)适合 H1 或 H4 周期的黄金交易。M15 以下周期建议减小马丁距离到 300-400 点,D1 周期建议加大到 1000-1500 点,并延长止损回溯到 20 根。
HalfTrend Martingale EA with Dynamic Stop & Trailing for MT5
An automated trading EA based on HalfTrend indicator arrow signals. Features dynamic stop-loss from 10-bar high/low, martingale position adding (max 3 layers) triggered at 600-point floating loss, trailing stop activated at 300-point profit, and a visual dashboard for monitoring.
Source code is shown above. Compiled EX5 files are available to VIP members.
评论
还没有人评论,来抢个沙发。