基于ATR的动态马丁EA

| 发表于 7 天前 | 显示全部楼层 |复制链接
这款马丁,基于ATR决定入场点,长期回测,无爆仓记录,克i呀测试使用,投资有风险,入场需谨慎

  1. //+------------------------------------------------------------------+
  2. //|                                                     ATR_Marti.mq5|
  3. //|                       ATR+对冲加倍+回撤保护(Hedging账户)         |
  4. //+------------------------------------------------------------------+
  5. #property stricta
  6. #property version   "1.10"
  7. #property description "基于ATR的对冲加倍策略,只在新K线做决策 + 回撤保护。需要Hedging账户。"
  8. #include <Trade/Trade.mqh>
  9. //================== 输入参数 ==================//
  10. input ENUM_TIMEFRAMES InpSignalTF         = PERIOD_CURRENT; // 触发决策的周期(新K线)
  11. input ENUM_TIMEFRAMES InpATRTF            = PERIOD_CURRENT; // ATR计算周期
  12. input int             InpATRPeriod        = 14;             // ATR周期
  13. input double          InpATRMultiplier    = 1.0;            // ATR倍数
  14. input bool            InpStartBuy         = true;           // 初始方向:true=BUY
  15. input double          InpBaseLot          = 0.01;           // 初始手数
  16. input double          InpMinProfitToClose = 0.0;            // 总浮盈达此值清仓(>0即盈利就平)
  17. input double          InpMaxLot           = 10.0;           // 最大手数上限
  18. input int             InpMaxSteps         = 20;             // 最大翻倍步数
  19. input bool            InpOnlyOnNewBar     = true;           // 只在新K线执行
  20. input int             InpSlippagePoints   = 10;             // 允许滑点(点)
  21. input long            InpMagic            = 26091701;        // 魔术号
  22. input ENUM_ORDER_TYPE_FILLING InpFilling  = ORDER_FILLING_IOC; // 成交策略
  23. //—— 回撤保护 ——//
  24. input bool   InpDD_Enable             = true;   // 开启回撤保护
  25. input double InpDD_MaxDailyPct        = 5.0;    // 日内最大回撤(%),0=关闭
  26. input double InpDD_MaxDailyMoney      = 0.0;    // 日内最大亏损(账户货币),0=关闭
  27. input double InpDD_MaxTotalPct        = 20.0;   // 历史最大回撤(%),0=关闭(基于权益高水位)
  28. input bool   InpDD_CloseAllOnTrigger  = true;   // 触发时是否立即清仓(本EA本品种)
  29. input bool   InpDD_BlockOpenUntilReset= true;   // 触发后禁止开新仓,直到重置
  30. input bool   InpDD_ResetOnNewDay      = true;   // 新的一天自动解除回撤阻断并重置日内基线
  31. input int    InpDD_PauseMinutes       = 0;      // 额外暂停分钟数(0=不使用暂停时间)
  32. //================== 全局对象与变量 ==================//
  33. CTrade trade;
  34. int    g_atr_handle = INVALID_HANDLE;
  35. datetime g_last_bar_time = 0;
  36. // 简单状态机
  37. enum SEQ_STATE
  38. {
  39.    STATE_SINGLE = 0,
  40.    STATE_MULTI_WAIT = 1,
  41.    STATE_AFTER_ADD_SAME = 2
  42. };
  43. int    g_state = STATE_SINGLE;
  44. string g_state_key;
  45. // 仓位结构
  46. struct PosInfo
  47. {
  48.    ulong               ticket;
  49.    datetime            time;
  50.    long                time_msc;
  51.    ENUM_POSITION_TYPE  type;
  52.    double              volume;
  53.    double              open_price;
  54.    double              profit;
  55. };
  56. double   g_point, g_tick_size, g_min_lot, g_max_lot, g_lot_step;
  57. //—— 回撤保护相关 ——//
  58. int      g_day_stamp = 0;           // yyyymmdd
  59. double   g_day_start_equity = 0.0;  // 日内基线权益
  60. double   g_equity_peak = 0.0;       // 历史权益高水位
  61. bool     g_dd_block_open = false;   // 是否阻止开新仓
  62. datetime g_dd_block_until = 0;      // 暂停到期时间(可选)
  63. //================== 工具函数 ==================//
  64. double NormalizeLot(double v)
  65. {
  66.    if(g_lot_step <= 0.0) return v;
  67.    double lot = MathRound(v / g_lot_step) * g_lot_step;
  68.    lot = MathMax(g_min_lot, MathMin(lot, MathMin(g_max_lot, InpMaxLot)));
  69.    return lot;
  70. }
  71. bool IsHedgingAccount()
  72. {
  73.    long mode = (long)AccountInfoInteger(ACCOUNT_MARGIN_MODE);
  74.    return (mode == ACCOUNT_MARGIN_MODE_RETAIL_HEDGING);
  75. }
  76. bool IsNewBar()
  77. {
  78.    datetime t = iTime(_Symbol, InpSignalTF, 0);
  79.    if(t == 0) return false;
  80.    if(g_last_bar_time == 0)
  81.    {
  82.       g_last_bar_time = t;
  83.       return true; // 首次允许动作
  84.    }
  85.    if(t != g_last_bar_time)
  86.    {
  87.       g_last_bar_time = t;
  88.       return true;
  89.    }
  90.    return false;
  91. }
  92. bool GetATR(double &atr_val)
  93. {
  94.    atr_val = 0.0;
  95.    if(g_atr_handle == INVALID_HANDLE) return false;
  96.    if(BarsCalculated(g_atr_handle) < InpATRPeriod+5) return false;
  97.    double buf[];
  98.    ArraySetAsSeries(buf, true);
  99.    if(CopyBuffer(g_atr_handle, 0, 1, 1, buf) <= 0) return false; // 上一根bar的ATR
  100.    atr_val = buf[0] * InpATRMultiplier;
  101.    return (atr_val > 0.0);
  102. }
  103. int GetPositions(PosInfo &arr[])
  104. {
  105.    ArrayResize(arr, 0);
  106.    int total = PositionsTotal();
  107.    for(int i=0;i<total;i++)
  108.    {
  109.       ulong ticket = PositionGetTicket(i);
  110.       if(ticket == 0) continue;
  111.       if(!PositionSelectByTicket(ticket)) continue;
  112.       string sym = PositionGetString(POSITION_SYMBOL);
  113.       long magic  = PositionGetInteger(POSITION_MAGIC);
  114.       if(sym != _Symbol || magic != InpMagic) continue;
  115.       PosInfo p;
  116.       p.ticket     = ticket;
  117.       p.type       = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
  118.       p.volume     = PositionGetDouble(POSITION_VOLUME);
  119.       p.open_price = PositionGetDouble(POSITION_PRICE_OPEN);
  120.       p.profit     = PositionGetDouble(POSITION_PROFIT);
  121.       p.time       = (datetime)PositionGetInteger(POSITION_TIME);
  122.       p.time_msc   = (long)PositionGetInteger(POSITION_TIME_MSC);
  123.       int n = ArraySize(arr);
  124.       ArrayResize(arr, n+1);
  125.       arr[n] = p;
  126.    }
  127.    // 按时间升序(最早在前)
  128.    int n = ArraySize(arr);
  129.    for(int i=0;i<n-1;i++)
  130.       for(int j=i+1;j<n;j++)
  131.          if(arr.time_msc > arr[j].time_msc)
  132.          {
  133.             PosInfo tmp = arr;
  134.             arr = arr[j];
  135.             arr[j] = tmp;
  136.          }
  137.    return ArraySize(arr);
  138. }
  139. double TotalProfit(PosInfo &arr[])
  140. {
  141.    double sum = 0.0;
  142.    for(int i=0;i<ArraySize(arr);i++)
  143.       sum += arr.profit;
  144.    return sum;
  145. }
  146. int DirFromType(ENUM_POSITION_TYPE t) { return (t == POSITION_TYPE_BUY ? +1 : -1); }
  147. // 与开仓价的距离(价格单位),BUY用Bid,SELL用Ask
  148. double DistanceFromOpenPrice(const PosInfo &p)
  149. {
  150.    double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
  151.    double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
  152.    if(p.type == POSITION_TYPE_BUY)
  153.       return (bid - p.open_price);
  154.    else
  155.       return (p.open_price - ask);
  156. }
  157. // 注意:comment 不用引用,避免“reference cannot be initialized”编译错误
  158. bool OpenByDir(int dir, double lots, const string comment = "")
  159. {
  160.    // 回撤保护:触发后可阻止开新仓
  161.    if(InpDD_Enable && InpDD_BlockOpenUntilReset && g_dd_block_open)
  162.    {
  163.       Print("DD protection active, skip opening: ", comment);
  164.       return false;
  165.    }
  166.    lots = NormalizeLot(lots);
  167.    if(lots < g_min_lot - 1e-12) return false;
  168.    trade.SetExpertMagicNumber(InpMagic);
  169.    trade.SetTypeFilling(InpFilling);
  170.    trade.SetDeviationInPoints(InpSlippagePoints);
  171.    bool ok = false;
  172.    if(dir > 0) ok = trade.Buy(lots, _Symbol, 0.0, 0.0, 0.0, comment);
  173.    else        ok = trade.Sell(lots, _Symbol, 0.0, 0.0, 0.0, comment);
  174.    if(!ok)
  175.       Print("OpenByDir failed. err=", GetLastError(), " lots=", lots, " dir=", dir);
  176.    return ok;
  177. }
  178. bool CloseByTicket(ulong ticket)
  179. {
  180.    trade.SetExpertMagicNumber(InpMagic);
  181.    trade.SetTypeFilling(InpFilling);
  182.    trade.SetDeviationInPoints(InpSlippagePoints);
  183.    bool ok = trade.PositionClose(ticket);
  184.    if(!ok)
  185.       Print("Close position failed ticket=", ticket, " err=", GetLastError());
  186.    return ok;
  187. }
  188. void CloseAll()
  189. {
  190.    ulong tickets[];
  191.    int total = PositionsTotal();
  192.    for(int i=0;i<total;i++)
  193.    {
  194.       ulong t = PositionGetTicket(i);
  195.       if(t == 0) continue;
  196.       if(!PositionSelectByTicket(t)) continue;
  197.       if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
  198.       if(PositionGetInteger(POSITION_MAGIC) != InpMagic) continue;
  199.       int n = ArraySize(tickets);
  200.       ArrayResize(tickets, n+1);
  201.       tickets[n] = t;
  202.    }
  203.    for(int i=0;i<ArraySize(tickets);i++)
  204.       CloseByTicket(tickets);
  205. }
  206. void SetState(int st)
  207. {
  208.    g_state = st;
  209.    if(g_state_key != "")
  210.       GlobalVariableSet(g_state_key, (double)st);
  211. }
  212. void RebuildStateFromPositions()
  213. {
  214.    PosInfo arr[];
  215.    int n = GetPositions(arr);
  216.    if(n <= 1) { SetState(STATE_SINGLE); return; }
  217.    SetState(STATE_MULTI_WAIT);
  218. }
  219. //—— 日期工具 ——//
  220. int DayStamp(datetime t)
  221. {
  222.    MqlDateTime dt; TimeToStruct(t, dt);
  223.    return dt.year*10000 + dt.mon*100 + dt.day;
  224. }
  225. // 每天0点或首次运行时,刷新日内权益基线;若设置“新的一天自动解除回撤阻断”,则一起解除
  226. void UpdateDailyAnchorIfNeeded(bool force=false)
  227. {
  228.    int ds = DayStamp(TimeCurrent());
  229.    if(force || g_day_stamp == 0 || (InpDD_ResetOnNewDay && ds != g_day_stamp))
  230.    {
  231.       g_day_stamp = ds;
  232.       g_day_start_equity = AccountInfoDouble(ACCOUNT_EQUITY);
  233.       if(InpDD_ResetOnNewDay)
  234.       {
  235.          g_dd_block_open = false;
  236.          g_dd_block_until = 0;
  237.       }
  238.       PrintFormat("Daily anchor reset. Day=%d, StartEquity=%.2f, Unblock=%s",
  239.                   g_day_stamp, g_day_start_equity, InpDD_ResetOnNewDay ? "Yes":"No");
  240.    }
  241. }
  242. // 检查回撤是否越界;越界则标记阻断,并可选清仓
  243. bool CheckDrawdownAndMaybeBlock(string &reason, double &dd_day_pct, double &dd_total_pct)
  244. {
  245.    reason = "";
  246.    dd_day_pct = 0.0; dd_total_pct = 0.0;
  247.    if(!InpDD_Enable) return false;
  248.    datetime now = TimeCurrent();
  249.    double equity = AccountInfoDouble(ACCOUNT_EQUITY);
  250.    // 更新高水位
  251.    if(g_equity_peak <= 0.0 || equity > g_equity_peak)
  252.       g_equity_peak = equity;
  253.    // 计算日内与历史回撤
  254.    double daily_loss_money = MathMax(0.0, g_day_start_equity - equity);
  255.    if(g_day_start_equity > 0) dd_day_pct = 100.0 * daily_loss_money / g_day_start_equity;
  256.    if(g_equity_peak > 0)      dd_total_pct = 100.0 * MathMax(0.0, g_equity_peak - equity) / g_equity_peak;
  257.    bool breach = false;
  258.    if(InpDD_MaxDailyPct > 0 && dd_day_pct >= InpDD_MaxDailyPct)         { breach = true; reason += (reason==""?"":"|"); reason += "DailyPct"; }
  259.    if(InpDD_MaxDailyMoney > 0 && daily_loss_money >= InpDD_MaxDailyMoney){ breach = true; reason += (reason==""?"":"|"); reason += "DailyMoney"; }
  260.    if(InpDD_MaxTotalPct > 0 && dd_total_pct >= InpDD_MaxTotalPct)        { breach = true; reason += (reason==""?"":"|"); reason += "TotalPct"; }
  261.    // 暂停时间到期自动解除
  262.    if(g_dd_block_open && InpDD_PauseMinutes > 0 && now >= g_dd_block_until)
  263.    {
  264.       g_dd_block_open = false;
  265.       g_dd_block_until = 0;
  266.       Print("DD pause expired. Trading unblocked.");
  267.    }
  268.    if(breach)
  269.    {
  270.       if(!g_dd_block_open)
  271.          PrintFormat("DD Breach: reason=%s, DayDD=%.2f%%, TotalDD=%.2f%%, equity=%.2f",
  272.                      reason, dd_day_pct, dd_total_pct, equity);
  273.       g_dd_block_open = true;
  274.       if(InpDD_PauseMinutes > 0)
  275.          g_dd_block_until = now + InpDD_PauseMinutes * 60;
  276.    }
  277.    return breach;
  278. }
  279. //================== 标准事件 ==================//
  280. int OnInit()
  281. {
  282.    trade.SetExpertMagicNumber(InpMagic);
  283.    trade.SetTypeFilling(InpFilling);
  284.    trade.SetDeviationInPoints(InpSlippagePoints);
  285.    g_point      = SymbolInfoDouble(_Symbol, SYMBOL_POINT);
  286.    g_tick_size  = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
  287.    g_min_lot    = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
  288.    g_max_lot    = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
  289.    g_lot_step   = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
  290.    if(!IsHedgingAccount())
  291.       Print("警告:当前账户不是Hedging模式,策略无法同时持有多空,将无法按策略执行。");
  292.    g_atr_handle = iATR(_Symbol, InpATRTF, InpATRPeriod);
  293.    if(g_atr_handle == INVALID_HANDLE)
  294.    {
  295.       Print("创建ATR失败");
  296.       return(INIT_FAILED);
  297.    }
  298.    g_state_key = StringFormat("ATR_Marti_State_%s_%I64d", _Symbol, InpMagic);
  299.    if(GlobalVariableCheck(g_state_key))
  300.       g_state = (int)GlobalVariableGet(g_state_key);
  301.    else
  302.       RebuildStateFromPositions();
  303.    g_last_bar_time = 0;
  304.    // 回撤保护基线
  305.    g_equity_peak = AccountInfoDouble(ACCOUNT_EQUITY);
  306.    UpdateDailyAnchorIfNeeded(true);
  307.    return(INIT_SUCCEEDED);
  308. }
  309. void OnDeinit(const int reason)
  310. {
  311.    if(g_atr_handle != INVALID_HANDLE)
  312.       IndicatorRelease(g_atr_handle);
  313. }
  314. void OnTick()
  315. {
  316.    // 1) 不受新K线限制的回撤保护(先执行)
  317.    UpdateDailyAnchorIfNeeded(false);
  318.    string dd_reason; double dd_day=0, dd_total=0;
  319.    bool dd_breach = CheckDrawdownAndMaybeBlock(dd_reason, dd_day, dd_total);
  320.    if((dd_breach || g_dd_block_open) && InpDD_CloseAllOnTrigger)
  321.    {
  322.       // 若有仓则清掉本EA在当前品种的所有仓位
  323.       PosInfo tmp[];
  324.       if(GetPositions(tmp) > 0)
  325.          CloseAll();
  326.       // 触发后不再执行本tick的策略逻辑(避免多余操作)
  327.       return;
  328.    }
  329.    // 2) 若设置只在新K线执行
  330.    if(InpOnlyOnNewBar && !IsNewBar())
  331.       return;
  332.    // 3) 正常策略逻辑
  333.    PosInfo arr[];
  334.    int n = GetPositions(arr);
  335.    // 无仓 -> 开启新一轮(若未被回撤阻断)
  336.    if(n == 0)
  337.    {
  338.       double lots = NormalizeLot(InpBaseLot);
  339.       int dir = InpStartBuy ? +1 : -1;
  340.       OpenByDir(dir, lots, "Start");
  341.       SetState(STATE_SINGLE);
  342.       return;
  343.    }
  344.    // 有仓 -> 计算ATR与总盈亏
  345.    double atr = 0.0;
  346.    if(!GetATR(atr)) return;
  347.    double totalProfit = TotalProfit(arr);
  348.    // 总浮盈达到目标,清仓重置
  349.    if(totalProfit >= InpMinProfitToClose)
  350.    {
  351.       CloseAll();
  352.       SetState(STATE_SINGLE);
  353.       return;
  354.    }
  355.    // 单仓位阶段
  356.    if(n == 1)
  357.    {
  358.       PosInfo p = arr[0];
  359.       double dist = DistanceFromOpenPrice(p);
  360.       if(dist >= atr) // 盈利超过ATR -> 平仓
  361.       {
  362.          CloseByTicket(p.ticket);
  363.          SetState(STATE_SINGLE);
  364.          return;
  365.       }
  366.       if(dist <= -atr) // 亏损超过ATR -> 反向加倍
  367.       {
  368.          double newLots = NormalizeLot(p.volume * 2.0);
  369.          if(InpMaxSteps > 0)
  370.          {
  371.             int steps = 0;
  372.             double v = InpBaseLot;
  373.             while(v + 1e-12 < newLots && steps < 100) { v *= 2.0; steps++; }
  374.             if(steps > InpMaxSteps)
  375.             {
  376.                Print("达到最大加仓步数,忽略加仓。");
  377.                return;
  378.             }
  379.          }
  380.          int dir = (p.type == POSITION_TYPE_BUY) ? -1 : +1; // 反向
  381.          OpenByDir(dir, newLots, "Add Opposite");
  382.          SetState(STATE_MULTI_WAIT);
  383.       }
  384.       return;
  385.    }
  386.    // 对冲阶段:两单或以上
  387.    if(totalProfit < InpMinProfitToClose)
  388.    {
  389.       if(g_state == STATE_MULTI_WAIT)
  390.       {
  391.          PosInfo first  = arr[0];
  392.          PosInfo latest = arr[ArraySize(arr)-1];
  393.          double first_dist = DistanceFromOpenPrice(first);
  394.          if(first_dist > 0) // 首单盈利 -> 平首单 + 同向加倍
  395.          {
  396.             CloseByTicket(first.ticket);
  397.             double newLots = NormalizeLot(latest.volume * 2.0);
  398.             int first_dir  = DirFromType(first.type);
  399.             if(InpMaxSteps > 0)
  400.             {
  401.                int steps = 0;
  402.                double v = InpBaseLot;
  403.                while(v + 1e-12 < newLots && steps < 100) { v *= 2.0; steps++; }
  404.                if(steps > InpMaxSteps)
  405.                {
  406.                   Print("达到最大加仓步数,忽略加仓。");
  407.                   return;
  408.                }
  409.             }
  410.             OpenByDir(first_dir, newLots, "Add SameAsFirst");
  411.             SetState(STATE_AFTER_ADD_SAME);
  412.             return;
  413.          }
  414.       }
  415.       else if(g_state == STATE_AFTER_ADD_SAME)
  416.       {
  417.          PosInfo latest = arr[ArraySize(arr)-1];
  418.          int latest_dir = DirFromType(latest.type);
  419.          int idx_to_close = -1;
  420.          for(int i=ArraySize(arr)-2; i>=0; i--)
  421.          {
  422.             if(DirFromType(arr.type) != latest_dir)
  423.             {
  424.                idx_to_close = i;
  425.                break;
  426.             }
  427.          }
  428.          if(idx_to_close != -1)
  429.          {
  430.             CloseByTicket(arr[idx_to_close].ticket);
  431.             SetState(STATE_MULTI_WAIT);
  432.             return;
  433.          }
  434.          else
  435.          {
  436.             SetState(STATE_SINGLE); // 已经同向
  437.          }
  438.       }
  439.    }
  440. }
复制代码
ScreenShot_2026-03-21_223313_078.png
ScreenShot_2026-03-21_223325_145.png
举报

评论 使用道具

精彩评论1

WHR-2025
D
| 发表于 2 小时前 | 显示全部楼层
好帖,顶上去
举报

点赞 评论 使用道具

发新帖
EA交易
您需要登录后才可以评论 登录 | 立即注册