670 lines
		
	
	
	
		
			41 KiB
		
	
	
	
		
			MQL5
		
	
	
	
	
	
			
		
		
	
	
			670 lines
		
	
	
	
		
			41 KiB
		
	
	
	
		
			MQL5
		
	
	
	
	
	
//+------------------------------------------------------------------+
 | 
						|
//| Trailing Stop EA                                                 |
 | 
						|
//+------------------------------------------------------------------+
 | 
						|
#property copyright "thRee03"
 | 
						|
#property link      "None"
 | 
						|
#property version   "8.88"
 | 
						|
#property strict
 | 
						|
 | 
						|
// 输入参数
 | 
						|
input bool EnableCloseAllSellOnBuy = false; // 开多单时全平空单,true=全平,false=不平
 | 
						|
input bool EnableOpenOrder = true; // 开单总开关,false时不开单但仍可移动止盈
 | 
						|
input int    Seconds = 133;        // 下单最小间隔秒数
 | 
						|
input double Lots = 0.01;          // 每次下单手数
 | 
						|
input int    Deviation = 10;       // 最大允许滑点(点)
 | 
						|
input double TP_Points = 10;       // 止盈点数(下单时设置TP)
 | 
						|
input int    MaxOrders = 13;       // 最大持仓单数
 | 
						|
input int    MagicNumber = 12345;  // 魔术编号
 | 
						|
input double ProfitTargetUSD = 13.0; // 单仓盈利目标(美元,满足即平仓)
 | 
						|
input bool   CloseOtherMagics = true; // 盈利平仓时是否包含其他魔术编号持仓
 | 
						|
input bool   EnableSAR = true;      // 是否启用SAR信号过滤(false=不开启任何SAR限制)
 | 
						|
input bool   iSAR_Reverse = false; // iSAR反向开单:true=只要SAR在K线上方或由下转上就开多,false=只要SAR在K线下方或由上转下就开多
 | 
						|
input double SAR_Acceleration = 0.007; // SAR加速度(建议0.01~0.03,趋势信号更灵敏)
 | 
						|
input double SAR_Start = 0.076;        // SAR初始值(0为默认)
 | 
						|
input double SAR_Maximum = 0.2;        // SAR最大值(建议0.1~0.2)
 | 
						|
input int    SAR_SmoothBars = 1;       // SAR平滑根数(取近N根均值)
 | 
						|
input double TrailingStopUSD = 3.3;    // 单仓利润回撤止盈(美元,移动止盈)
 | 
						|
input bool   EnableDrawdownOpen = true;    // 启用历史高点回撤加仓(true=启用,false=关闭)
 | 
						|
input double DrawdownOpenUSD = 10.0;     // 距离历史最高价回撤N美元时开多单
 | 
						|
input bool   EnableAddOrderEveryUSD = true; // 启用每跌N美元加仓(true=启用,false=关闭)
 | 
						|
input double AddOrderEveryUSD = 5.0;     // 下单后每下跌N美元再加一单
 | 
						|
input int DrawdownMaxOrders = 3;         // 历史高点回撤加仓最大单数(独立于MaxOrders)
 | 
						|
input bool TrailingStopOnlyThisMagic = true; // 只对本魔术号仓位移动止盈(true=只本EA,false=所有BUY仓位)
 | 
						|
input double MinBalance = 1200.0;      // 最低账户余额(低于不下单)
 | 
						|
 | 
						|
// 全局变量
 | 
						|
double g_point = 0.0;
 | 
						|
int    g_digits = 0;
 | 
						|
double g_min_lot = 0.0;
 | 
						|
double g_lot_step = 0.0;
 | 
						|
double g_max_lot = 0.0;
 | 
						|
int    g_filling_mode = 0;
 | 
						|
int    orderCount = 0;
 | 
						|
datetime lastOrderTime = 0;
 | 
						|
double g_highest_price = 0.0; // 历史最高价
 | 
						|
double g_last_add_price = 0.0; // 上次加仓价
 | 
						|
 | 
						|
// 工具函数
 | 
						|
double AlignToStep(double val, double step, bool up=false)
 | 
						|
{
 | 
						|
   if(step <= 0.0) return val;
 | 
						|
   double k = up ? MathCeil(val/step) : MathFloor(val/step);
 | 
						|
   double v = k*step;
 | 
						|
   v = NormalizeDouble(v, 2);
 | 
						|
   return MathMax(step, v);
 | 
						|
}
 | 
						|
 | 
						|
double AlignToPoint(double price)
 | 
						|
{
 | 
						|
   if(g_point <= 0) return price;
 | 
						|
   return NormalizeDouble(MathRound(price/g_point)*g_point, g_digits);
 | 
						|
}
 | 
						|
 | 
						|
int GetCurrentPositions(const string sym, const ulong magic)
 | 
						|
{
 | 
						|
   int count = 0;
 | 
						|
   int total = (int)PositionsTotal();
 | 
						|
   for(int i=0; i<total; ++i)
 | 
						|
   {
 | 
						|
      ulong ticket = PositionGetTicket(i);
 | 
						|
      if(!PositionSelectByTicket(ticket)) continue;
 | 
						|
      if(PositionGetString(POSITION_SYMBOL) == sym &&
 | 
						|
         PositionGetInteger(POSITION_MAGIC) == (long)magic &&
 | 
						|
         PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY)
 | 
						|
      {
 | 
						|
         count++;
 | 
						|
      }
 | 
						|
   }
 | 
						|
   return count;
 | 
						|
}
 | 
						|
 | 
						|
// 统计当前EA持仓中注释为DD的单数(回撤加仓单)
 | 
						|
int GetDrawdownOrderCount(const string sym, const ulong magic)
 | 
						|
{
 | 
						|
   int count = 0;
 | 
						|
   int total = (int)PositionsTotal();
 | 
						|
   for(int i=0; i<total; ++i)
 | 
						|
   {
 | 
						|
      ulong ticket = PositionGetTicket(i);
 | 
						|
      if(!PositionSelectByTicket(ticket)) continue;
 | 
						|
      if(PositionGetString(POSITION_SYMBOL) == sym &&
 | 
						|
         PositionGetInteger(POSITION_MAGIC) == (long)magic &&
 | 
						|
         PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY)
 | 
						|
      {
 | 
						|
         string comment = PositionGetString(POSITION_COMMENT);
 | 
						|
         if(comment == "DD")
 | 
						|
            count++;
 | 
						|
      }
 | 
						|
   }
 | 
						|
   return count;
 | 
						|
}
 | 
						|
void CheckAndCloseProfitablePositions()
 | 
						|
{
 | 
						|
   double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
 | 
						|
   if(tickValue <= 0) return;
 | 
						|
 | 
						|
   // 获取当前SAR值
 | 
						|
   double sarValue = 0.0;
 | 
						|
   double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
 | 
						|
   if(ask > 0.0)
 | 
						|
   {
 | 
						|
      double sarSum = 0.0;
 | 
						|
      int sarCount = 0;
 | 
						|
      for(int i=1; i<=SAR_SmoothBars; ++i)
 | 
						|
      {
 | 
						|
         double v = CustomSAR(i, SAR_Acceleration, SAR_Maximum, SAR_Start);
 | 
						|
         if(v > 0.0 && v < ask * 100 && v > ask / 100)
 | 
						|
         {
 | 
						|
            sarSum += v;
 | 
						|
            sarCount++;
 | 
						|
         }
 | 
						|
      }
 | 
						|
      if(sarCount > 0)
 | 
						|
         sarValue = sarSum / sarCount;
 | 
						|
   }
 | 
						|
 | 
						|
   int total = (int)PositionsTotal();
 | 
						|
   int closedCount = 0;
 | 
						|
   for(int i=total-1; i>=0; --i)
 | 
						|
   {
 | 
						|
      ulong ticket = PositionGetTicket(i);
 | 
						|
      if(!PositionSelectByTicket(ticket)) continue;
 | 
						|
      if(PositionGetString(POSITION_SYMBOL) != _Symbol ||
 | 
						|
         PositionGetInteger(POSITION_TYPE) != POSITION_TYPE_BUY) continue;
 | 
						|
      if(!CloseOtherMagics && PositionGetInteger(POSITION_MAGIC) != (long)MagicNumber) continue;
 | 
						|
 | 
						|
      double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);
 | 
						|
      double volume = PositionGetDouble(POSITION_VOLUME);
 | 
						|
      MqlTick tick;
 | 
						|
      if(!SymbolInfoTick(_Symbol, tick)) continue;
 | 
						|
      double currentPrice = tick.bid;
 | 
						|
      double profit = ((currentPrice - openPrice) / g_point) * volume * tickValue;
 | 
						|
 | 
						|
   Print("[CheckProfit] Ticket:", ticket, " Magic:", PositionGetInteger(POSITION_MAGIC), " Profit:", profit, " USD, Target:", ProfitTargetUSD, " SAR:", sarValue, " Price:", currentPrice);
 | 
						|
 | 
						|
      if(profit >= ProfitTargetUSD)
 | 
						|
      {
 | 
						|
         // 平仓
 | 
						|
         MqlTradeRequest req; MqlTradeResult res;
 | 
						|
         ZeroMemory(req); ZeroMemory(res);
 | 
						|
         req.action = TRADE_ACTION_DEAL;
 | 
						|
         req.symbol = _Symbol;
 | 
						|
         req.type = ORDER_TYPE_SELL;
 | 
						|
         req.volume = volume;
 | 
						|
         req.price = currentPrice;
 | 
						|
         req.deviation = Deviation;
 | 
						|
         req.type_filling = GetFillingMode();
 | 
						|
         req.magic = PositionGetInteger(POSITION_MAGIC); // 使用原魔术编号
 | 
						|
         req.position = ticket;
 | 
						|
 | 
						|
         if(OrderSend(req, res) && (res.retcode == TRADE_RETCODE_DONE || res.retcode == TRADE_RETCODE_PLACED))
 | 
						|
         {
 | 
						|
            Print("Closed profitable position. Ticket: ", ticket, " Profit: ", profit, " USD, Magic: ", PositionGetInteger(POSITION_MAGIC));
 | 
						|
            closedCount++;
 | 
						|
         }
 | 
						|
         else
 | 
						|
         {
 | 
						|
            Print("Failed to close position: ", res.retcode);
 | 
						|
         }
 | 
						|
      }
 | 
						|
   }
 | 
						|
   if(closedCount == 0) {
 | 
						|
      Print("[CheckProfit] No profitable positions found to close.");
 | 
						|
   } else {
 | 
						|
      Print("[CheckProfit] Closed ", closedCount, " profitable positions.");
 | 
						|
   }
 | 
						|
}
 | 
						|
 | 
						|
void CloseAllSellPositions()
 | 
						|
{
 | 
						|
   int total = (int)PositionsTotal();
 | 
						|
   for(int i=total-1; i>=0; --i)
 | 
						|
   {
 | 
						|
      ulong ticket = PositionGetTicket(i);
 | 
						|
      if(!PositionSelectByTicket(ticket)) continue;
 | 
						|
      if(PositionGetString(POSITION_SYMBOL) != _Symbol ||
 | 
						|
         PositionGetInteger(POSITION_TYPE) != POSITION_TYPE_SELL) continue;
 | 
						|
 | 
						|
      double volume = PositionGetDouble(POSITION_VOLUME);
 | 
						|
      MqlTick tick;
 | 
						|
      if(!SymbolInfoTick(_Symbol, tick)) continue;
 | 
						|
      double currentPrice = tick.ask; // 平 SELL 用 ask
 | 
						|
 | 
						|
      // 平仓
 | 
						|
      MqlTradeRequest req; MqlTradeResult res;
 | 
						|
      ZeroMemory(req); ZeroMemory(res);
 | 
						|
      req.action = TRADE_ACTION_DEAL;
 | 
						|
      req.symbol = _Symbol;
 | 
						|
      req.type = ORDER_TYPE_BUY;
 | 
						|
      req.volume = volume;
 | 
						|
      req.price = currentPrice;
 | 
						|
      req.deviation = Deviation;
 | 
						|
      req.type_filling = GetFillingMode();
 | 
						|
      req.magic = PositionGetInteger(POSITION_MAGIC);
 | 
						|
      req.position = ticket;
 | 
						|
 | 
						|
      if(OrderSend(req, res) && (res.retcode == TRADE_RETCODE_DONE || res.retcode == TRADE_RETCODE_PLACED))
 | 
						|
      {
 | 
						|
         Print("Closed SELL position. Ticket: ", ticket, " Magic: ", PositionGetInteger(POSITION_MAGIC));
 | 
						|
      }
 | 
						|
      else
 | 
						|
      {
 | 
						|
         Print("Failed to close SELL position: ", res.retcode);
 | 
						|
      }
 | 
						|
   }
 | 
						|
}
 | 
						|
 | 
						|
void TrailingStop()
 | 
						|
{
 | 
						|
   if(TrailingStopUSD <= 0) return;
 | 
						|
 | 
						|
   double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
 | 
						|
   if(tickValue <= 0) return;
 | 
						|
 | 
						|
   int total = (int)PositionsTotal();
 | 
						|
   for(int i=0; i<total; ++i)
 | 
						|
   {
 | 
						|
      ulong ticket = PositionGetTicket(i);
 | 
						|
      if(!PositionSelectByTicket(ticket)) continue;
 | 
						|
      if(PositionGetString(POSITION_SYMBOL) != _Symbol ||
 | 
						|
         PositionGetInteger(POSITION_TYPE) != POSITION_TYPE_BUY) continue;
 | 
						|
      if(TrailingStopOnlyThisMagic && PositionGetInteger(POSITION_MAGIC) != (long)MagicNumber) continue;
 | 
						|
 | 
						|
      // 独立获取每个持仓的开仓价、手数、当前止损
 | 
						|
      double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);
 | 
						|
      double currentSL = PositionGetDouble(POSITION_SL);
 | 
						|
      double volume = PositionGetDouble(POSITION_VOLUME);
 | 
						|
      MqlTick tick;
 | 
						|
      if(!SymbolInfoTick(_Symbol, tick)) continue;
 | 
						|
      double currentPrice = tick.bid;
 | 
						|
 | 
						|
      // 计算该持仓当前盈利
 | 
						|
      double profitUSD = ((currentPrice - openPrice) / g_point) * volume * tickValue;
 | 
						|
 | 
						|
      // 计算该持仓应设置的止损价
 | 
						|
      double newSL = currentSL;
 | 
						|
      if(profitUSD > TrailingStopUSD)
 | 
						|
      {
 | 
						|
         double priceRetracement = TrailingStopUSD / (volume * tickValue) * g_point;
 | 
						|
         double candidateSL = currentPrice - priceRetracement;
 | 
						|
         candidateSL = AlignToPoint(candidateSL);
 | 
						|
         candidateSL = MathMax(candidateSL, openPrice); // 止损不低于开仓价
 | 
						|
         if(candidateSL > currentSL || currentSL == 0.0)
 | 
						|
            newSL = candidateSL; // 只允许上移或首次设置
 | 
						|
      }
 | 
						|
      // 盈利未达阈值或回撤,不下移止损
 | 
						|
 | 
						|
      // 仅当止损需要上移时才发出修改请求
 | 
						|
      if(newSL > currentSL)
 | 
						|
      {
 | 
						|
         MqlTradeRequest req; MqlTradeResult res;
 | 
						|
         ZeroMemory(req); ZeroMemory(res);
 | 
						|
         req.action = TRADE_ACTION_SLTP;
 | 
						|
         req.symbol = _Symbol;
 | 
						|
         req.position = ticket;
 | 
						|
         req.sl = newSL;
 | 
						|
         req.tp = PositionGetDouble(POSITION_TP); // 保留原TP
 | 
						|
         req.magic = PositionGetInteger(POSITION_MAGIC);
 | 
						|
 | 
						|
         if(OrderSend(req, res) && (res.retcode == TRADE_RETCODE_DONE || res.retcode == TRADE_RETCODE_PLACED))
 | 
						|
         {
 | 
						|
            Print("Trailing stop updated for position ", ticket, " to ", newSL, " (Profit: ", profitUSD, " USD), TP kept: ", req.tp);
 | 
						|
         }
 | 
						|
         else
 | 
						|
         {
 | 
						|
            Print("Failed to update trailing stop: ", res.retcode);
 | 
						|
         }
 | 
						|
      }
 | 
						|
   }
 | 
						|
}
 | 
						|
 | 
						|
bool LoadSymbolSpec()
 | 
						|
{
 | 
						|
   g_point  = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
 | 
						|
   g_digits = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS);
 | 
						|
   g_min_lot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
 | 
						|
   g_lot_step = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
 | 
						|
   g_max_lot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
 | 
						|
   g_filling_mode = (int)SymbolInfoInteger(_Symbol, SYMBOL_FILLING_MODE);
 | 
						|
 | 
						|
   if(g_point<=0 || g_min_lot<=0 || g_lot_step<=0 || g_max_lot<=0)
 | 
						|
   {
 | 
						|
      Print("Spec load failed");
 | 
						|
      return false;
 | 
						|
   }
 | 
						|
   return true;
 | 
						|
}
 | 
						|
 | 
						|
ENUM_ORDER_TYPE_FILLING GetFillingMode()
 | 
						|
{
 | 
						|
   return ORDER_FILLING_IOC; // 强制使用 IOC
 | 
						|
}
 | 
						|
 | 
						|
// 自定义SAR算法(支持初始量)
 | 
						|
double CustomSAR(int barsBack, double step, double max, double start)
 | 
						|
{
 | 
						|
   int bars = Bars(_Symbol, PERIOD_CURRENT);
 | 
						|
   if(bars < 2 + barsBack) return 0.0;
 | 
						|
   
 | 
						|
   double sar = 0.0;
 | 
						|
   double af = (start > 0.0 ? start : step);
 | 
						|
   bool uptrend;
 | 
						|
   double ep;
 | 
						|
   int idx = barsBack + 1;
 | 
						|
   double high1 = iHigh(_Symbol, PERIOD_CURRENT, idx);
 | 
						|
   double low1 = iLow(_Symbol, PERIOD_CURRENT, idx);
 | 
						|
   double close1 = iClose(_Symbol, PERIOD_CURRENT, idx);
 | 
						|
   double high0 = iHigh(_Symbol, PERIOD_CURRENT, idx-1);
 | 
						|
   double low0 = iLow(_Symbol, PERIOD_CURRENT, idx-1);
 | 
						|
   double close0 = iClose(_Symbol, PERIOD_CURRENT, idx-1);
 | 
						|
   // 初始化趋势
 | 
						|
   if(close0 > close1)
 | 
						|
   {
 | 
						|
      uptrend = true;
 | 
						|
      ep = high0;
 | 
						|
      sar = low1 + af * (high0 - low1);
 | 
						|
   }
 | 
						|
   else
 | 
						|
   {
 | 
						|
      uptrend = false;
 | 
						|
      ep = low0;
 | 
						|
      sar = high1 + af * (low0 - high1);
 | 
						|
   }
 | 
						|
   // 回溯到当前bar
 | 
						|
   for(int i=idx-2; i>=barsBack; --i)
 | 
						|
   {
 | 
						|
      double high = iHigh(_Symbol, PERIOD_CURRENT, i);
 | 
						|
      double low = iLow(_Symbol, PERIOD_CURRENT, i);
 | 
						|
      if(uptrend)
 | 
						|
      {
 | 
						|
         sar = sar + af * (ep - sar);
 | 
						|
         if(low < sar)
 | 
						|
         {
 | 
						|
            uptrend = false;
 | 
						|
            sar = ep;
 | 
						|
            ep = low;
 | 
						|
            af = (start > 0.0 ? start : step);
 | 
						|
         }
 | 
						|
         else
 | 
						|
         {
 | 
						|
            if(high > ep)
 | 
						|
            {
 | 
						|
               ep = high;
 | 
						|
               af = MathMin(af + step, max);
 | 
						|
            }
 | 
						|
         }
 | 
						|
         sar = MathMin(sar, low);
 | 
						|
      }
 | 
						|
      else
 | 
						|
      {
 | 
						|
         sar = sar + af * (ep - sar);
 | 
						|
         if(high > sar)
 | 
						|
         {
 | 
						|
            uptrend = true;
 | 
						|
            sar = ep;
 | 
						|
            ep = high;
 | 
						|
            af = (start > 0.0 ? start : step);
 | 
						|
         }
 | 
						|
         else
 | 
						|
         {
 | 
						|
            if(low < ep)
 | 
						|
            {
 | 
						|
               ep = low;
 | 
						|
               af = MathMin(af + step, max);
 | 
						|
            }
 | 
						|
         }
 | 
						|
         sar = MathMax(sar, high);
 | 
						|
      }
 | 
						|
   }
 | 
						|
   return sar;
 | 
						|
}
 | 
						|
 | 
						|
// EA 初始化
 | 
						|
int OnInit()
 | 
						|
{
 | 
						|
   if(!LoadSymbolSpec())
 | 
						|
   {
 | 
						|
      Print("Failed to load symbol spec");
 | 
						|
      return INIT_FAILED;
 | 
						|
   }
 | 
						|
   orderCount = 0;
 | 
						|
   lastOrderTime = 0;
 | 
						|
   Print("EA initialized. Max orders: ", MaxOrders, ", Interval: ", Seconds, " seconds, Magic: ", MagicNumber);
 | 
						|
   return INIT_SUCCEEDED;
 | 
						|
}
 | 
						|
 | 
						|
// EA 反初始化
 | 
						|
void OnDeinit(const int reason)
 | 
						|
{
 | 
						|
   // 无需操作
 | 
						|
}
 | 
						|
 | 
						|
// EA 主循环
 | 
						|
void OnTick()
 | 
						|
{
 | 
						|
   TrailingStop(); // 移动止盈
 | 
						|
   CheckAndCloseProfitablePositions(); // 检查并平仓盈利仓位
 | 
						|
 | 
						|
   // 获取当前Ask价
 | 
						|
   MqlTick tick;
 | 
						|
   if(!SymbolInfoTick(_Symbol, tick))
 | 
						|
   {
 | 
						|
      Print("Failed to get tick");
 | 
						|
      return;
 | 
						|
   }
 | 
						|
   double ask = tick.ask;
 | 
						|
 | 
						|
   // 记录历史最高价
 | 
						|
   if(g_highest_price < ask || g_highest_price == 0.0)
 | 
						|
      g_highest_price = ask;
 | 
						|
 | 
						|
   // 计算距离历史高点的回撤
 | 
						|
   double drawdown = g_highest_price - ask;
 | 
						|
 | 
						|
   // 检查余额
 | 
						|
   double currentBalance = AccountInfoDouble(ACCOUNT_BALANCE);
 | 
						|
   if(currentBalance < MinBalance)
 | 
						|
   {
 | 
						|
      Print("Balance too low: ", currentBalance, " < ", MinBalance, ", not opening order");
 | 
						|
      return;
 | 
						|
   }
 | 
						|
 | 
						|
   int currentPositions = GetCurrentPositions(_Symbol, MagicNumber);
 | 
						|
   int remainingOrders = MaxOrders - currentPositions;
 | 
						|
   if(remainingOrders <= 0) return; // 已达最大持仓
 | 
						|
 | 
						|
   // 历史高点回撤下单(不受时间间隔限制,受DrawdownMaxOrders限制)
 | 
						|
   bool canOpenDrawdown = false;
 | 
						|
   int drawdownOrders = GetDrawdownOrderCount(_Symbol, MagicNumber);
 | 
						|
   if(EnableOpenOrder && EnableDrawdownOpen && DrawdownOpenUSD > 0 && drawdown >= DrawdownOpenUSD && drawdownOrders < DrawdownMaxOrders)
 | 
						|
   {
 | 
						|
      canOpenDrawdown = true;
 | 
						|
   }
 | 
						|
 | 
						|
   // 每跌N美元加仓(不受时间间隔限制,受MaxOrders限制)
 | 
						|
   bool canAddOrder = false;
 | 
						|
   if(EnableOpenOrder && EnableAddOrderEveryUSD && AddOrderEveryUSD > 0 && currentPositions > 0 && currentPositions < MaxOrders)
 | 
						|
   {
 | 
						|
      if(g_last_add_price == 0.0) g_last_add_price = ask; // 首次赋值
 | 
						|
      double priceDrop = g_last_add_price - ask;
 | 
						|
      if(priceDrop >= AddOrderEveryUSD)
 | 
						|
         canAddOrder = true;
 | 
						|
   }
 | 
						|
 | 
						|
   // 只要满足任一条件就下单(不受时间间隔限制)
 | 
						|
   // 回撤加仓受DrawdownMaxOrders限制,加仓受MaxOrders限制
 | 
						|
   if((canOpenDrawdown && drawdownOrders < DrawdownMaxOrders) || (canAddOrder && currentPositions < MaxOrders))
 | 
						|
   {
 | 
						|
      // 对齐手数
 | 
						|
      double vol = Lots;
 | 
						|
      vol = MathMax(g_min_lot, vol);
 | 
						|
      vol = AlignToStep(vol, g_lot_step);
 | 
						|
      vol = MathMin(vol, g_max_lot);
 | 
						|
      if(vol < g_min_lot - 1e-8)
 | 
						|
      {
 | 
						|
         Print("Volume too small");
 | 
						|
         return;
 | 
						|
      }
 | 
						|
 | 
						|
      // 对齐价格
 | 
						|
      double price = AlignToPoint(ask);
 | 
						|
 | 
						|
      // 计算TP
 | 
						|
      double tp_price = ask + TP_Points;
 | 
						|
      tp_price = AlignToPoint(tp_price);
 | 
						|
 | 
						|
      // 选择合约
 | 
						|
      if(!SymbolSelect(_Symbol, true))
 | 
						|
      {
 | 
						|
         Print("Symbol not selectable");
 | 
						|
         return;
 | 
						|
      }
 | 
						|
 | 
						|
      // 发送订单
 | 
						|
      MqlTradeRequest req;
 | 
						|
      MqlTradeResult  res;
 | 
						|
      ZeroMemory(req);
 | 
						|
      ZeroMemory(res);
 | 
						|
 | 
						|
      req.action       = TRADE_ACTION_DEAL;
 | 
						|
      req.symbol       = _Symbol;
 | 
						|
      req.type         = ORDER_TYPE_BUY;
 | 
						|
      req.volume       = vol;
 | 
						|
      req.price        = price;
 | 
						|
      req.sl           = 0.0;
 | 
						|
      req.tp           = tp_price;
 | 
						|
      req.deviation    = Deviation;
 | 
						|
      req.type_filling = GetFillingMode();
 | 
						|
      req.magic        = MagicNumber;
 | 
						|
      // 注释区分:回撤加仓写DD,其余为空
 | 
						|
      if(canOpenDrawdown && drawdownOrders < DrawdownMaxOrders)
 | 
						|
         req.comment = "DD";
 | 
						|
      else
 | 
						|
         req.comment = "";
 | 
						|
 | 
						|
      if(OrderSend(req, res))
 | 
						|
      {
 | 
						|
         if(res.retcode == TRADE_RETCODE_DONE || res.retcode == TRADE_RETCODE_PLACED)
 | 
						|
         {
 | 
						|
            orderCount++;
 | 
						|
            lastOrderTime = TimeCurrent();
 | 
						|
            g_last_add_price = ask; // 记录本次加仓价
 | 
						|
            Print("[DD/Add] BUY order ", orderCount, " placed. Drawdown=", drawdown, " AddOrderEveryUSD=", AddOrderEveryUSD, " Ask=", ask, " Highest=", g_highest_price, " currentPositions=", currentPositions+1, "/", MaxOrders, " comment=", req.comment);
 | 
						|
            if(EnableCloseAllSellOnBuy)
 | 
						|
               CloseAllSellPositions();
 | 
						|
         }
 | 
						|
         else
 | 
						|
         {
 | 
						|
            Print("Order failed: ", res.retcode, " Ask: ", ask);
 | 
						|
         }
 | 
						|
      }
 | 
						|
      else
 | 
						|
      {
 | 
						|
         Print("OrderSend failed, error code: ", GetLastError());
 | 
						|
      }
 | 
						|
      return; // 本tick只下一个单
 | 
						|
   }
 | 
						|
 | 
						|
   // 其余原有下单逻辑(受时间间隔、SAR等限制)
 | 
						|
   // 取近SAR_SmoothBars根bar的自定义SAR均值,提升平滑度
 | 
						|
   double sarSum = 0.0;
 | 
						|
   int sarCount = 0;
 | 
						|
   for(int i=1; i<=SAR_SmoothBars; ++i)
 | 
						|
   {
 | 
						|
      double v = CustomSAR(i, SAR_Acceleration, SAR_Maximum, SAR_Start);
 | 
						|
      if(v > 0.0 && v < ask * 100 && v > ask / 100) // 有效性保护
 | 
						|
      {
 | 
						|
         sarSum += v;
 | 
						|
         sarCount++;
 | 
						|
      }
 | 
						|
   }
 | 
						|
   double sarValue = (sarCount > 0) ? (sarSum / sarCount) : 0.0;
 | 
						|
 | 
						|
   // SAR值有效性保护
 | 
						|
   if(sarValue <= 0.0 || sarValue > ask * 100 || sarValue < ask / 100)
 | 
						|
   {
 | 
						|
      Print("[Warn] 平滑CustomSAR均值异常,跳过本次开单。sarValue=", sarValue, " ask=", ask);
 | 
						|
      return;
 | 
						|
   }
 | 
						|
 | 
						|
   // 获取当前RSI值
 | 
						|
   int rsiHandle = iRSI(_Symbol, PERIOD_CURRENT, 14, PRICE_CLOSE);
 | 
						|
   if(rsiHandle == INVALID_HANDLE)
 | 
						|
   {
 | 
						|
      Print("Failed to create RSI handle");
 | 
						|
      return;
 | 
						|
   }
 | 
						|
   double rsi[1];
 | 
						|
   if(CopyBuffer(rsiHandle, 0, 0, 1, rsi) != 1)
 | 
						|
   {
 | 
						|
      Print("Failed to copy RSI buffer");
 | 
						|
      IndicatorRelease(rsiHandle);
 | 
						|
      return;
 | 
						|
   }
 | 
						|
   IndicatorRelease(rsiHandle);
 | 
						|
 | 
						|
   // 其余原有下单逻辑
 | 
						|
   if(lastOrderTime > 0 && TimeCurrent() - lastOrderTime < Seconds) return; // 未到间隔时间
 | 
						|
 | 
						|
   // SAR信号过滤
 | 
						|
   bool sarSignal = true;
 | 
						|
   if(EnableSAR)
 | 
						|
   {
 | 
						|
      sarSignal = false;
 | 
						|
      double prevSar = 0.0, prevClose = 0.0;
 | 
						|
      // 取上一根bar的SAR均值
 | 
						|
      double sarSum2 = 0.0;
 | 
						|
      int sarCount2 = 0;
 | 
						|
      for(int i=2; i<=SAR_SmoothBars+1; ++i)
 | 
						|
      {
 | 
						|
         double v = CustomSAR(i, SAR_Acceleration, SAR_Maximum, SAR_Start);
 | 
						|
         if(v > 0.0 && v < ask * 100 && v > ask / 100)
 | 
						|
         {
 | 
						|
            sarSum2 += v;
 | 
						|
            sarCount2++;
 | 
						|
         }
 | 
						|
      }
 | 
						|
      prevSar = (sarCount2 > 0) ? (sarSum2 / sarCount2) : 0.0;
 | 
						|
      double closeBuf[2];
 | 
						|
      if(CopyClose(_Symbol, PERIOD_CURRENT, 0, 2, closeBuf) == 2)
 | 
						|
      {
 | 
						|
         prevClose = closeBuf[1];
 | 
						|
         if(iSAR_Reverse)
 | 
						|
         {
 | 
						|
            // true:只要SAR在K线上方 或 由下转上就开多
 | 
						|
            if(sarValue > ask) sarSignal = true; // SAR在K线上方
 | 
						|
            else if(prevSar < prevClose && sarValue > ask) sarSignal = true; // 由下转上
 | 
						|
         }
 | 
						|
         else
 | 
						|
         {
 | 
						|
            // false:只要SAR在K线下方 或 由上转下就开多
 | 
						|
            if(sarValue < ask) sarSignal = true; // SAR在K线下方
 | 
						|
            else if(prevSar > prevClose && sarValue < ask) sarSignal = true; // 由上转下
 | 
						|
         }
 | 
						|
      }
 | 
						|
   }
 | 
						|
   if(!sarSignal) return;
 | 
						|
 | 
						|
   // 对齐手数
 | 
						|
   double vol = Lots;
 | 
						|
   vol = MathMax(g_min_lot, vol);
 | 
						|
   vol = AlignToStep(vol, g_lot_step);
 | 
						|
   vol = MathMin(vol, g_max_lot);
 | 
						|
   if(vol < g_min_lot - 1e-8)
 | 
						|
   {
 | 
						|
      Print("Volume too small");
 | 
						|
      return;
 | 
						|
   }
 | 
						|
 | 
						|
   // 对齐价格
 | 
						|
   double price = AlignToPoint(ask);
 | 
						|
 | 
						|
   // 计算TP
 | 
						|
   double tp_price = ask + TP_Points;
 | 
						|
   tp_price = AlignToPoint(tp_price);
 | 
						|
 | 
						|
   // 选择合约
 | 
						|
   if(!SymbolSelect(_Symbol, true))
 | 
						|
   {
 | 
						|
      Print("Symbol not selectable");
 | 
						|
      return;
 | 
						|
   }
 | 
						|
 | 
						|
   // 发送订单
 | 
						|
   MqlTradeRequest req;
 | 
						|
   MqlTradeResult  res;
 | 
						|
   ZeroMemory(req);
 | 
						|
   ZeroMemory(res);
 | 
						|
 | 
						|
   req.action       = TRADE_ACTION_DEAL;
 | 
						|
   req.symbol       = _Symbol;
 | 
						|
   req.type         = ORDER_TYPE_BUY;
 | 
						|
   req.volume       = vol;
 | 
						|
   req.price        = price;
 | 
						|
   req.sl           = 0.0;
 | 
						|
   req.tp           = tp_price;
 | 
						|
   req.deviation    = Deviation;
 | 
						|
   req.type_filling = GetFillingMode();
 | 
						|
   req.magic        = MagicNumber;
 | 
						|
 | 
						|
   if(OrderSend(req, res))
 | 
						|
   {
 | 
						|
      if(res.retcode == TRADE_RETCODE_DONE || res.retcode == TRADE_RETCODE_PLACED)
 | 
						|
      {
 | 
						|
         orderCount++;
 | 
						|
         lastOrderTime = TimeCurrent();
 | 
						|
         Print("BUY order ", orderCount, " placed successfully. Volume: ", vol, " Price: ", price, " TP: ", tp_price, " TP Spread: ", TP_Points, " Ask: ", ask, " SAR: ", sarValue, " (Remaining: ", remainingOrders-1, ")");
 | 
						|
         if(EnableCloseAllSellOnBuy)
 | 
						|
            CloseAllSellPositions(); // 根据参数决定是否平空单
 | 
						|
      }
 | 
						|
      else
 | 
						|
      {
 | 
						|
         Print("Order failed: ", res.retcode, " Ask: ", ask, " SAR: ", sarValue);
 | 
						|
      }
 | 
						|
   }
 | 
						|
   else
 | 
						|
   {
 | 
						|
      Print("OrderSend failed, error code: ", GetLastError());
 | 
						|
   }
 | 
						|
}
 | 
						|
//+------------------------------------------------------------------+
 |