//+------------------------------------------------------------------+
//|                                                         mj.mq5   |
//|                                                     Copyright 2026 |
//|                                             https://www.mql5.com  |
//+------------------------------------------------------------------+
#property copyright "Copyright 2026"
#property link      "https://www.mql5.com"
#property version   "1.00"
#property strict

// 包含交易类库
#include <Trade\Trade.mqh>
// 包含按钮控件库
#include <Controls\Button.mqh>

//--- 全局交易对象
CTrade trade;                    // 交易操作对象

//--- 按钮对象定义
CButton btnBuy;                  // 买入按钮
CButton btnSell;                 // 卖出按钮
CButton btnCloseBuy;             // 平多按钮
CButton btnCloseSell;            // 平空按钮

//--- 指标句柄
int hSARDir     = INVALID_HANDLE;   // 方向周期SAR句柄
int hBBDir      = INVALID_HANDLE;   // 方向周期布林带句柄
int hBBEntry    = INVALID_HANDLE;   // 进场周期布林带句柄
int hATREntry   = INVALID_HANDLE;   // 进场周期ATR句柄
int hSARExit    = INVALID_HANDLE;   // SAR转向平仓周期SAR句柄

//--- 全局变量
datetime g_lastEntryBarTime = 0;    // 进场周期最新K线时间(新K线判断)

//--- 图表周期参数
input ENUM_TIMEFRAMES InpTFDir      = PERIOD_H1;   // 方向确认周期(默认H1)
input ENUM_TIMEFRAMES InpTFEntry    = PERIOD_M5;   // 进场确认周期(默认M5)
input ENUM_TIMEFRAMES InpTFSARExit  = PERIOD_M15;  // SAR转向平仓周期(默认M15)

//--- 方向周期指标参数(SAR+布林带)
input double InpSARStep             = 0.02;        // SAR步长
input double InpSARMaximum          = 0.2;         // SAR最大值
input int    InpBBDirPeriod         = 20;          // 方向周期布林带周期
input double InpBBDirDeviation      = 2.0;         // 方向周期布林带偏差

//--- 进场周期指标参数(布林带+ATR)
input int    InpBBEntryPeriod       = 20;          // 进场周期布林带周期
input double InpBBEntryDeviation    = 2.0;         // 进场周期布林带偏差
input int    InpATRPeriod           = 14;          // 进场周期ATR周期

//--- 基础交易参数
input double InpLots                = 0.01;        // 基础开仓手数
input long   InpMagic               = 123456;      // EA魔术号
input int    InpSlippage            = 30;          // 最大滑点(点)

//--- 马丁加仓参数
input double InpAddDistATRCoeff     = 2.0;         // 加仓距离系数(进场周期ATR倍数)
input int    InpMaxAddCount         = 5;           // 马丁最大加仓次数
input double InpMartinLotMult       = 2.0;         // 加仓手数倍数

//--- 止盈止损参数
input double InpSLATRCoeff          = 3.0;         // 止损距离系数(进场周期ATR倍数)
input double InpTPATRCoeff          = 1.5;         // 止盈距离系数(进场周期ATR倍数)
input double InpMinSLPoints         = 300;         // 最小止损距离(点)
input double InpMinTPPoints         = 200;         // 最小止盈距离(点)

//--- 总盈总损参数
input double InpTotalProfitTarget   = 100.0;       // 总盈利金额阈值(货币, 0=不启用)
input double InpTotalLossLimit      = 200.0;       // 总亏损金额阈值(货币, 0=不启用)

//+------------------------------------------------------------------+
//| 判断是否为进场周期的新K线                                            |
//+------------------------------------------------------------------+
bool IsNewEntryBar()
{
   datetime barTime = iTime(_Symbol, InpTFEntry, 0);   // 当前K线时间
   if(barTime != g_lastEntryBarTime)                   // 时间变化即为新K线
   {
      g_lastEntryBarTime = barTime;                    // 记录最新K线时间
      return true;
   }
   return false;
}

//+------------------------------------------------------------------+
//| 获取进场周期ATR值                                                   |
//+------------------------------------------------------------------+
double GetEntryATR()
{
   double buf[];
   if(CopyBuffer(hATREntry, 0, 1, 1, buf) < 1)         // 取上一根已完成K线的ATR
      return 0.0;                                      // 获取失败返回0
   return buf[0];
}

//+------------------------------------------------------------------+
//| 获取方向周期趋势方向: 1=只允许多, -1=只允许空, 0=无方向               |
//| SAR在下方且价格处于中轨与上轨之间 -> 只多                             |
//| SAR在上方且价格处于中轨与下轨之间 -> 只空                             |
//+------------------------------------------------------------------+
int GetTrendDirection()
{
   double sarBuf[], upperBuf[], midBuf[], lowerBuf[];

   //--- 使用方向周期上一根已完成K线数据, 避免盘中信号跳动
   if(CopyBuffer(hSARDir, 0, 1, 1, sarBuf)   < 1) return 0;   // SAR值
   if(CopyBuffer(hBBDir, 1, 1, 1, upperBuf)  < 1) return 0;   // 布林带上轨
   if(CopyBuffer(hBBDir, 0, 1, 1, midBuf)    < 1) return 0;   // 布林带中轨
   if(CopyBuffer(hBBDir, 2, 1, 1, lowerBuf)  < 1) return 0;   // 布林带下轨

   double closePrice = iClose(_Symbol, InpTFDir, 1);          // 方向周期收盘价
   if(closePrice <= 0) return 0;

   //--- 多头方向: SAR在价格下方 且 收盘价位于中轨和上轨之间
   if(sarBuf[0] < closePrice && closePrice > midBuf[0] && closePrice < upperBuf[0])
      return 1;

   //--- 空头方向: SAR在价格上方 且 收盘价位于中轨和下轨之间
   if(sarBuf[0] > closePrice && closePrice < midBuf[0] && closePrice > lowerBuf[0])
      return -1;

   return 0;                                                  // 其他情况不交易
}

//+------------------------------------------------------------------+
//| 统计本EA持仓数量及均价                                               |
//+------------------------------------------------------------------+
bool GetBasketInfo(int &count, ENUM_POSITION_TYPE &posType, double &avgPrice)
{
   count = 0;                                   // 持仓笔数
   double priceVolume = 0.0;                    // 价格*手数累计
   double volumeSum   = 0.0;                    // 手数累计

   for(int i = PositionsTotal() - 1; i >= 0; i--)
   {
      ulong ticket = PositionGetTicket(i);      // 选中持仓并获取票号
      if(ticket <= 0) continue;                 // 无效跳过
      if(PositionGetInteger(POSITION_MAGIC) != InpMagic) continue;   // 魔术号不符跳过
      if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;    // 品种不符跳过

      posType = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
      double vol = PositionGetDouble(POSITION_VOLUME);
      priceVolume += PositionGetDouble(POSITION_PRICE_OPEN) * vol;
      volumeSum   += vol;
      count++;
   }

   if(count == 0 || volumeSum <= 0) return false;   // 空仓返回false
   avgPrice = priceVolume / volumeSum;              // 加权平均持仓价
   return true;
}

//+------------------------------------------------------------------+
//| 计算本EA持仓浮动总盈亏(含库存费)                                      |
//+------------------------------------------------------------------+
double GetFloatingTotal()
{
   double total = 0.0;
   for(int i = PositionsTotal() - 1; i >= 0; i--)
   {
      ulong ticket = PositionGetTicket(i);
      if(ticket <= 0) continue;
      if(PositionGetInteger(POSITION_MAGIC) != InpMagic) continue;
      if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;

      total += PositionGetDouble(POSITION_PROFIT) + PositionGetDouble(POSITION_SWAP);
   }
   return total;
}

//+------------------------------------------------------------------+
//| 平掉本EA所有持仓                                                    |
//+------------------------------------------------------------------+
void CloseAllOurPositions()
{
   for(int i = PositionsTotal() - 1; i >= 0; i--)     // 从最后一单向前遍历
   {
      ulong ticket = PositionGetTicket(i);
      if(ticket <= 0) continue;
      if(PositionGetInteger(POSITION_MAGIC) != InpMagic) continue;
      if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;

      trade.PositionClose(ticket);                    // 逐笔平仓
   }
}

//+------------------------------------------------------------------+
//| 平仓函数：根据方向平掉所有该方向持仓(面板手动用)                        |
//+------------------------------------------------------------------+
void ClosePositions(ENUM_POSITION_TYPE posType)
{
   for(int i = PositionsTotal() - 1; i >= 0; i--)     // 从最后一单向前遍历
   {
      ulong ticket = PositionGetTicket(i);
      if(ticket <= 0) continue;
      if(PositionGetInteger(POSITION_MAGIC) != InpMagic) continue;
      if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
      if((ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE) != posType) continue;

      trade.PositionClose(ticket);
   }
}

//+------------------------------------------------------------------+
//| 手数规范化                                                          |
//+------------------------------------------------------------------+
double NormalizeLots(double lots)
{
   double minLot  = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);    // 最小手数
   double maxLot  = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);    // 最大手数
   double lotStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);   // 手数步长

   if(lotStep > 0) lots = MathFloor(lots / lotStep) * lotStep;       // 按步长向下取整
   if(lots < minLot) lots = minLot;                                  // 不小于最小手数
   if(lots > maxLot) lots = maxLot;                                  // 不大于最大手数
   return lots;
}

//+------------------------------------------------------------------+
//| 计算止盈止损价格: 进场周期ATR*系数, 不小于设定最小点数                  |
//+------------------------------------------------------------------+
bool CalcSLTP(bool isBuy, double &sl, double &tp)
{
   double atr = GetEntryATR();                          // 进场周期ATR
   if(atr <= 0) return false;                           // ATR无效则失败

   double slDist = MathMax(atr * InpSLATRCoeff, InpMinSLPoints * _Point);   // 止损距离
   double tpDist = MathMax(atr * InpTPATRCoeff, InpMinTPPoints * _Point);   // 止盈距离

   if(isBuy)
   {
      double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
      sl = NormalizeDouble(ask - slDist, _Digits);     // 多单止损
      tp = NormalizeDouble(ask + tpDist, _Digits);     // 多单止盈
   }
   else
   {
      double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
      sl = NormalizeDouble(bid + slDist, _Digits);     // 空单止损
      tp = NormalizeDouble(bid - tpDist, _Digits);     // 空单止盈
   }
   return true;
}

//+------------------------------------------------------------------+
//| 市价开仓函数                                                        |
//+------------------------------------------------------------------+
void OpenMarket(ENUM_ORDER_TYPE orderType, double lots, string comment)
{
   double sl = 0, tp = 0;
   if(!CalcSLTP(orderType == ORDER_TYPE_BUY, sl, tp))   // 计算止盈止损
      return;

   bool result = false;
   if(orderType == ORDER_TYPE_BUY)
      result = trade.Buy(lots, _Symbol, 0, sl, tp, comment);    // 市价买入
   else
      result = trade.Sell(lots, _Symbol, 0, sl, tp, comment);   // 市价卖出

   if(result)
      PrintFormat("%s 成交: 手数=%.2f 止损=%s 止盈=%s",
                  comment, lots,
                  DoubleToString(sl, _Digits), DoubleToString(tp, _Digits));
   else
      Print(comment, " 失败: ", trade.ResultRetcode(), " ", trade.ResultRetcodeDescription());
}

//+------------------------------------------------------------------+
//| 获取SAR转向平仓周期SAR方向: 1=SAR在价格下方(多), -1=在上方(空), 0=无效   |
//+------------------------------------------------------------------+
int GetSARExitDirection()
{
   double sarBuf[];
   if(CopyBuffer(hSARExit, 0, 1, 1, sarBuf) < 1) return 0;    // 上一根已完成K线SAR
   double closePrice = iClose(_Symbol, InpTFSARExit, 1);      // 同K线收盘价
   if(closePrice <= 0) return 0;

   if(sarBuf[0] < closePrice) return 1;                       // SAR在下方 -> 多头状态
   if(sarBuf[0] > closePrice) return -1;                      // SAR在上方 -> 空头状态
   return 0;
}

//+------------------------------------------------------------------+
//| SAR转向平仓: SAR翻转方向与持仓方向相反时平掉该方向全部持仓               |
//+------------------------------------------------------------------+
void CheckSARReverseExit()
{
   int sarDir = GetSARExitDirection();
   if(sarDir == 0) return;                                    // SAR数据无效

   for(int i = PositionsTotal() - 1; i >= 0; i--)             // 从最后一单向前遍历
   {
      ulong ticket = PositionGetTicket(i);
      if(ticket <= 0) continue;
      if(PositionGetInteger(POSITION_MAGIC) != InpMagic) continue;
      if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;

      ENUM_POSITION_TYPE posType = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);

      //--- 多单遇到SAR翻到上方(转空)则平仓, 空单遇到SAR翻到下方(转多)则平仓
      if((posType == POSITION_TYPE_BUY && sarDir == -1) ||
         (posType == POSITION_TYPE_SELL && sarDir == 1))
      {
         if(trade.PositionClose(ticket))
            Print("SAR转向平仓: ", posType == POSITION_TYPE_BUY ? "多单" : "空单", " 票号=", ticket);
         else
            Print("SAR转向平仓失败: ", trade.ResultRetcodeDescription());
      }
   }
}

//+------------------------------------------------------------------+
//| 自动开仓: 方向周期定方向 + 进场周期布林带定时机                         |
//+------------------------------------------------------------------+
void TryAutoOpen()
{
   //--- 进场周期布林带信号(使用上一根已完成K线)
   double upperBuf[], lowerBuf[];
   if(CopyBuffer(hBBEntry, 1, 1, 1, upperBuf) < 1) return;   // 进场周期上轨
   if(CopyBuffer(hBBEntry, 2, 1, 1, lowerBuf) < 1) return;   // 进场周期下轨

   double closePrice = iClose(_Symbol, InpTFEntry, 1);       // 进场周期收盘价
   if(closePrice <= 0) return;

   int dir = GetTrendDirection();                            // 方向周期趋势过滤

   //--- 收盘价跌破下轨且方向只允许多 -> 开多
   if(closePrice < lowerBuf[0] && dir == 1)
      OpenMarket(ORDER_TYPE_BUY, NormalizeLots(InpLots), "MJ自动开多");

   //--- 收盘价突破上轨且方向只允许空 -> 开空
   else if(closePrice > upperBuf[0] && dir == -1)
      OpenMarket(ORDER_TYPE_SELL, NormalizeLots(InpLots), "MJ自动开空");
}

//+------------------------------------------------------------------+
//| 马丁加仓: 逆行加仓距离 = 进场周期ATR * 系数                            |
//+------------------------------------------------------------------+
void TryMartingaleAdd()
{
   int count = 0;
   ENUM_POSITION_TYPE posType;
   double avgPrice = 0;

   if(!GetBasketInfo(count, posType, avgPrice)) return;      // 空仓直接返回
   if(count > InpMaxAddCount) return;                        // 已达最大加仓次数(首单+N次加仓)

   double atr = GetEntryATR();
   if(atr <= 0) return;

   double addDist = atr * InpAddDistATRCoeff;                // 加仓距离
   double lots = NormalizeLots(InpLots * MathPow(InpMartinLotMult, count));   // 马丁递增手数

   if(posType == POSITION_TYPE_BUY)                          // 多单逆行加仓
   {
      double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
      if(avgPrice - ask >= addDist)
         OpenMarket(ORDER_TYPE_BUY, lots, "MJ马丁加多" + IntegerToString(count));
   }
   else                                                      // 空单逆行加仓
   {
      double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
      if(bid - avgPrice >= addDist)
         OpenMarket(ORDER_TYPE_SELL, lots, "MJ马丁加空" + IntegerToString(count));
   }
}

//+------------------------------------------------------------------+
//| 总盈总损检查: 达到金额阈值全部平仓                                    |
//+------------------------------------------------------------------+
void CheckTotalExit()
{
   int count = 0;
   ENUM_POSITION_TYPE posType;
   double avgPrice = 0;
   if(!GetBasketInfo(count, posType, avgPrice)) return;      // 空仓无需检查

   double floating = GetFloatingTotal();                     // 浮动总盈亏

   //--- 总盈利达标
   if(InpTotalProfitTarget > 0 && floating >= InpTotalProfitTarget)
   {
      Print("总盈利达到阈值, 全部平仓: ", DoubleToString(floating, 2));
      CloseAllOurPositions();
   }
   //--- 总亏损达标
   else if(InpTotalLossLimit > 0 && floating <= -InpTotalLossLimit)
   {
      Print("总亏损达到阈值, 全部平仓: ", DoubleToString(floating, 2));
      CloseAllOurPositions();
   }
}

//+------------------------------------------------------------------+
//| 面板初始化函数，创建4个按钮                                         |
//+------------------------------------------------------------------+
void CreatePanel()
{
   const int panelX = 10, panelY = 30;
   const int btnW = 80, btnH = 30, gap = 10;

   int x = panelX, y = panelY;

   btnBuy.Create(0, "btnBuy", 0, x, y, x + btnW, y + btnH);
   btnBuy.Text("Buy");
   btnBuy.Color(clrGreen);
   btnBuy.FontSize(10);

   x += btnW + gap;
   btnSell.Create(0, "btnSell", 0, x, y, x + btnW, y + btnH);
   btnSell.Text("Sell");
   btnSell.Color(clrRed);
   btnSell.FontSize(10);

   y += btnH + gap;
   x = panelX;

   btnCloseBuy.Create(0, "btnCloseBuy", 0, x, y, x + btnW, y + btnH);
   btnCloseBuy.Text("CloseBuy");
   btnCloseBuy.Color(clrYellow);
   btnCloseBuy.FontSize(10);

   x += btnW + gap;
   btnCloseSell.Create(0, "btnCloseSell", 0, x, y, x + btnW, y + btnH);
   btnCloseSell.Text("CloseSell");
   btnCloseSell.Color(clrYellow);
   btnCloseSell.FontSize(10);
}

//+------------------------------------------------------------------+
//| 手动开仓：仅用ATR算止盈止损，不做条件过滤                            |
//+------------------------------------------------------------------+
void TryManualOpenPosition(ENUM_ORDER_TYPE orderType)
{
   if(orderType == ORDER_TYPE_BUY)
      OpenMarket(ORDER_TYPE_BUY, NormalizeLots(InpLots), "手动开多");
   else
      OpenMarket(ORDER_TYPE_SELL, NormalizeLots(InpLots), "手动开空");
}

//+------------------------------------------------------------------+
//| Expert initialization function                                    |
//+------------------------------------------------------------------+
int OnInit()
{
   //--- 设置交易对象的魔术号
   trade.SetExpertMagicNumber(InpMagic);            // 设置魔术号
   trade.SetDeviationInPoints(InpSlippage);         // 设置滑点
   trade.SetTypeFillingBySymbol(_Symbol);           // 按品种自动选择成交方式

   //--- 创建方向周期SAR指标
   hSARDir = iSAR(_Symbol, InpTFDir, InpSARStep, InpSARMaximum);
   if(hSARDir == INVALID_HANDLE)
   {
      Print("方向周期SAR指标创建失败");
      return INIT_FAILED;
   }

   //--- 创建方向周期布林带指标
   hBBDir = iBands(_Symbol, InpTFDir, InpBBDirPeriod, 0, InpBBDirDeviation, PRICE_CLOSE);
   if(hBBDir == INVALID_HANDLE)
   {
      Print("方向周期布林带指标创建失败");
      return INIT_FAILED;
   }

   //--- 创建进场周期布林带指标
   hBBEntry = iBands(_Symbol, InpTFEntry, InpBBEntryPeriod, 0, InpBBEntryDeviation, PRICE_CLOSE);
   if(hBBEntry == INVALID_HANDLE)
   {
      Print("进场周期布林带指标创建失败");
      return INIT_FAILED;
   }

   //--- 创建进场周期ATR指标
   hATREntry = iATR(_Symbol, InpTFEntry, InpATRPeriod);
   if(hATREntry == INVALID_HANDLE)
   {
      Print("进场周期ATR指标创建失败");
      return INIT_FAILED;
   }

   //--- 创建SAR转向平仓周期SAR指标
   hSARExit = iSAR(_Symbol, InpTFSARExit, InpSARStep, InpSARMaximum);
   if(hSARExit == INVALID_HANDLE)
   {
      Print("SAR转向平仓周期SAR指标创建失败");
      return INIT_FAILED;
   }

   //--- 品种提示
   if(StringFind(_Symbol, "XAU") < 0)
      Print("提示: 本EA针对XAUUSD设计, 当前图表品种为", _Symbol);

   //--- 创建面板按钮
   CreatePanel();

   g_lastEntryBarTime = 0;                          // 重置新K线记录
   Print("EA初始化成功");
   return INIT_SUCCEEDED;
}

//+------------------------------------------------------------------+
//| Expert deinitialization function                                  |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
   //--- 释放指标句柄
   if(hSARDir   != INVALID_HANDLE) IndicatorRelease(hSARDir);
   if(hBBDir    != INVALID_HANDLE) IndicatorRelease(hBBDir);
   if(hBBEntry  != INVALID_HANDLE) IndicatorRelease(hBBEntry);
   if(hATREntry != INVALID_HANDLE) IndicatorRelease(hATREntry);
   if(hSARExit  != INVALID_HANDLE) IndicatorRelease(hSARExit);

   //--- 删除面板按钮
   ObjectDelete(0, "btnBuy");                       // 删除买入按钮
   ObjectDelete(0, "btnSell");                      // 删除卖出按钮
   ObjectDelete(0, "btnCloseBuy");                  // 删除平多按钮
   ObjectDelete(0, "btnCloseSell");                 // 删除平空按钮

   Print("EA已卸载");
}

//+------------------------------------------------------------------+
//| Expert tick function                                              |
//+------------------------------------------------------------------+
void OnTick()
{
   //--- 1. 总盈总损检查, 达到阈值全部平仓
   CheckTotalExit();

   //--- 2. SAR转向平仓检查(默认M15周期, 可设定)
   CheckSARReverseExit();

   //--- 3. 空仓时检查自动开仓信号, 有仓时检查马丁加仓
   int count = 0;
   ENUM_POSITION_TYPE posType;
   double avgPrice = 0;

   if(GetBasketInfo(count, posType, avgPrice))
      TryMartingaleAdd();                            // 持仓中: 马丁逆行加仓
   else if(IsNewEntryBar())                          // 空仓: 新K线时检查信号
      TryAutoOpen();
}

//+------------------------------------------------------------------+
//| ChartEvent function - 处理按钮点击事件                             |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
{
   if(id != CHARTEVENT_OBJECT_CLICK) return;

   if(sparam == "btnBuy" || sparam == "btnSell")
   {
      ENUM_ORDER_TYPE type = (sparam == "btnBuy") ? ORDER_TYPE_BUY : ORDER_TYPE_SELL;
      TryManualOpenPosition(type);
   }
   else if(sparam == "btnCloseBuy")
   {
      ClosePositions(POSITION_TYPE_BUY);
      Print("手动平多仓");
   }
   else if(sparam == "btnCloseSell")
   {
      ClosePositions(POSITION_TYPE_SELL);
      Print("手动平空仓");
   }

   ObjectSetInteger(0, sparam, OBJPROP_STATE, false);
   ChartRedraw();
}
//+------------------------------------------------------------------+
