MACD_Scout/Kerangka-EA.md
2025-11-14 10:21:06 +00:00

13 KiB

Berikut struktur/kerangka kode MQL5 untuk EA scalping MACD + Slow Stochastic sesuai deskripsi (entry setelah candle konfirmasi, SL/TP 1R/2R dengan partial 80/20, SL ke breakeven setelah TP1).

Kerangka file EA (SimpleSkeleton.mq5)

//+------------------------------------------------------------------+
//|                                                MACD_Stoch_Scalp  |
//|                                                (kerangka MQL5)   |
//+------------------------------------------------------------------+
#property copyright ""
#property version   "1.0"
#property strict

#include <Trade/Trade.mqh>
CTrade trade;

//-------------------------------
// Input parameter
//-------------------------------
input ENUM_TIMEFRAMES InpTF = PERIOD_M5;
input int   MACD_Fast   = 12;
input int   MACD_Slow   = 26;
input int   MACD_Signal = 9;

input int   Stoch_K = 14;
input int   Stoch_D = 3;
input int   Stoch_Slowing = 3;
input double Stoch_OB = 80.0;   // overbought
input double Stoch_OS = 20.0;   // oversold

input double Risk_Percent = 1.0; // risiko per posisi (% ekuitas)
input double Min_SL_Pips  = 5;   // SL minimum
input bool   Use_Partial  = true;
input double Partial_Close_Percent = 80.0; // tutup 80% di TP1
input ulong  Magic = 20251114;

// Filter biaya & sesi
input double Max_Spread_Points = 30;
input bool   Use_Session_Filter = true;
input string Session_Start = "07:00:00";
input string Session_End   = "23:00:00";

//-------------------------------
// Global handle & state
//-------------------------------
int macdHandle = INVALID_HANDLE;
int stochHandle = INVALID_HANDLE;

datetime lastBarTime = 0;

//+------------------------------------------------------------------+
//| OnInit                                                           |
//+------------------------------------------------------------------+
int OnInit()
{
  trade.SetExpertMagicNumber(Magic);

  macdHandle = iMACD(_Symbol, InpTF, MACD_Fast, MACD_Slow, MACD_Signal, PRICE_CLOSE);
  stochHandle = iStochastic(_Symbol, InpTF, Stoch_K, Stoch_D, Stoch_Slowing, MODE_SMA, STO_LOWHIGH);

  if(macdHandle==INVALID_HANDLE || stochHandle==INVALID_HANDLE)
  {
    Print("Indicator handle creation failed");
    return(INIT_FAILED);
  }
  return(INIT_SUCCEEDED);
}

//+------------------------------------------------------------------+
//| OnDeinit                                                         |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
  if(macdHandle!=INVALID_HANDLE) IndicatorRelease(macdHandle);
  if(stochHandle!=INVALID_HANDLE) IndicatorRelease(stochHandle);
}

//+------------------------------------------------------------------+
//| OnTick                                                           |
//+------------------------------------------------------------------+
void OnTick()
{
  if(!IsNewBar(InpTF)) return;

  // Perbarui data indikator
  double macdMain[^3], macdSignal[^3], macdHist[^3];
  if(!ReadMACD(3, macdMain, macdSignal, macdHist)) return;

  double k[^3], d[^3];
  if(!ReadStoch(3, k, d)) return;

  // Filter spread & sesi
  if(!CostFilterOK() || !SessionOK()) return;

  // Evaluasi setup
  bool buySetup=false, sellSetup=false;
  EvaluateSetups(macdHist, k, d, buySetup, sellSetup);

  // Eksekusi
  if(buySetup) TryBuyEntry();
  if(sellSetup) TrySellEntry();

  // Manajemen posisi berjalan
  ManageOpenPositions();
}

//-------------------------------
// Utilitas Bar Baru
//-------------------------------
bool IsNewBar(ENUM_TIMEFRAMES tf)
{
  datetime t = iTime(_Symbol, tf, 0);
  if(t==0) return false;
  if(t!=lastBarTime)
  {
    lastBarTime = t;
    return true;
  }
  return false;
}

//-------------------------------
// Baca MACD (Main/Signal/Hist)
//-------------------------------
bool ReadMACD(int count, double &main[], double &signal[], double &hist[])
{
  ArraySetAsSeries(main,true);
  ArraySetAsSeries(signal,true);
  ArraySetAsSeries(hist,true);

  int copied1 = CopyBuffer(macdHandle, 0, 0, count, main);
  int copied2 = CopyBuffer(macdHandle, 1, 0, count, signal);
  int copied3 = CopyBuffer(macdHandle, 2, 0, count, hist);
  return (copied1==count && copied2==count && copied3==count);
}

//-------------------------------
// Baca Stochastic (K/D)
//-------------------------------
bool ReadStoch(int count, double &k[], double &d[])
{
  ArraySetAsSeries(k,true);
  ArraySetAsSeries(d,true);

  int copiedK = CopyBuffer(stochHandle, 0, 0, count, k);
  int copiedD = CopyBuffer(stochHandle, 1, 0, count, d);
  return (copiedK==count && copiedD==count);
}

//-------------------------------
// Filter biaya & sesi
//-------------------------------
bool CostFilterOK()
{
  double spr = 0.0;
  if(!SymbolInfoDouble(_Symbol, SYMBOL_SPREAD, spr)) return false;
  // SYMBOL_SPREAD sudah dalam points pada MT5
  return spr <= Max_Spread_Points;
}

bool SessionOK()
{
  if(!Use_Session_Filter) return true;
  datetime now = TimeCurrent();
  MqlDateTime mt; TimeToStruct(now, mt);

  datetime s = StringToTime(Session_Start);
  datetime e = StringToTime(Session_End);

  // Normalisasi ke hari berjalan
  MqlDateTime ms; TimeToStruct(s, ms);
  ms.year = mt.year; ms.mon = mt.mon; ms.day = mt.day;
  MqlDateTime me; TimeToStruct(e, me);
  me.year = mt.year; me.mon = mt.mon; me.day = mt.day;

  datetime start = StructToTime(ms);
  datetime end   = StructToTime(me);

  if(start<=end) return (now>=start && now<=end);
  // sesi lintas tengah malam
  return (now>=start || now<=end);
}

//-------------------------------
// Evaluasi setup entry
// Aturan (ringkas) dari dokumen: 
// Long: hist > 0, sempat melemah lalu rebound naik (sebelum 0),
//       Stoch <=20 dan cross up; entry setelah candle konfirmasi tutup.
// Short kebalikan (hist < 0, rebound turun, Stoch >=80 cross down).
//-------------------------------
void EvaluateSetups(const double &macdHist[], const double &k[], const double &d[],
                    bool &buySetup, bool &sellSetup)
{
  buySetup=false; sellSetup=false;

  // Sinyal MACD histogram
  bool macdUpBias   = (macdHist[^0] > 0.0);
  bool macdDownBias = (macdHist[^0] < 0.0);

  // Deteksi "melemah lalu rebound" sederhana memakai tiga bar
  bool reboundUp   = (macdHist[^2] > macdHist   && macdHist   < macdHist[^0] && macdHist[^0] > 0.0);
  bool reboundDown = (macdHist[^2] < macdHist   && macdHist   > macdHist[^0] && macdHist[^0] < 0.0);

  // Stochastic cross
  bool stochCrossUp   = (k   < d   && k[^0] > d[^0]);
  bool stochCrossDown = (k   > d   && k[^0] < d[^0]);

  bool stochOS = (k[^0] <= Stoch_OS && d[^0] <= Stoch_OS);
  bool stochOB = (k[^0] >= Stoch_OB && d[^0] >= Stoch_OB);

  // Long
  if(macdUpBias && reboundUp && stochOS && stochCrossUp)
    buySetup = true;

  // Short
  if(macdDownBias && reboundDown && stochOB && stochCrossDown)
    sellSetup = true;
}

//-------------------------------
// Position sizing dan SL/TP
//-------------------------------
double PipToPoints(double pips)
{
  // Untuk simbol dengan fractional pip, pips ke points = pips * 10 (umum pada forex 5-digit)
  // Sesuaikan jika perlu.
  return pips * 10.0;
}

double CalcSLPoints(bool isBuy)
{
  // SL berdasar base candle (bar konfirmasi, index 1)
  double baseHigh = iHigh(_Symbol, InpTF, 1);
  double baseLow  = iLow(_Symbol,  InpTF, 1);
  double slPips = MathMax(Min_SL_Pips, 0.0);

  if(isBuy)
    return ( (NormalizeDouble((iClose(_Symbol, InpTF, 1) - (baseLow - (slPips*_Point*10.0))), _Digits)) / _Point ); // pendekatan
  else
    return ( (NormalizeDouble(((baseHigh + (slPips*_Point*10.0)) - iClose(_Symbol, InpTF, 1)), _Digits)) / _Point );
}

// Versi sederhana: hitung lot berdasar Risk_Percent dan SL points aproksimasi
double CalcLots(double sl_points)
{
  double equity = AccountInfoDouble(ACCOUNT_EQUITY);
  double riskMoney = equity * (Risk_Percent/100.0);
  double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
  double tickSize  = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
  if(tickValue<=0 || tickSize<=0 || sl_points<=0) return 0.01;

  // Perkiraan nilai per point per lot
  double pointValuePerLot = tickValue * (_Point / tickSize);
  double lots = riskMoney / (sl_points * pointValuePerLot);
  // Sesuaikan ke step & min lot
  double lotStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
  double minLot  = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
  double maxLot  = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
  lots = MathMax(minLot, MathMin(maxLot, MathFloor(lots/lotStep)*lotStep));
  return lots;
}

//-------------------------------
// Eksekusi order
//-------------------------------
void TryBuyEntry()
{
  // Hitung SL berbasis base candle: 1 pip di bawah low
  double baseLow  = iLow(_Symbol,  InpTF, 1);
  double entry    = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
  double slPrice  = baseLow - (Min_SL_Pips * _Point * 10.0);
  if(slPrice<=0 || entry<=0) return;

  double sl_points = (entry - slPrice) / _Point;
  if(sl_points < PipToPoints(Min_SL_Pips)) sl_points = PipToPoints(Min_SL_Pips);

  double lots = CalcLots(sl_points);
  if(lots <= 0) return;

  double tp1Price = entry + sl_points * _Point;          // 1R
  double tp2Price = entry + (2.0 * sl_points) * _Point;  // 2R

  trade.Buy(lots, NULL, entry, slPrice, 0.0, "MACD_Stoch_LONG");

  // Partial: pasang TP1 via pending take-profit manual atau kelola via ManageOpenPositions()
  // Di sini, kita kelola via manajemen per-tick untuk fleksibilitas.
}

void TrySellEntry()
{
  double baseHigh = iHigh(_Symbol, InpTF, 1);
  double entry    = SymbolInfoDouble(_Symbol, SYMBOL_BID);
  double slPrice  = baseHigh + ((Min_SL_Pips) * _Point * 10.0); // tambahkan spread bisa diperhitungkan di Min_SL_Pips
  if(slPrice<=0 || entry<=0) return;

  double sl_points = (slPrice - entry) / _Point;
  if(sl_points < PipToPoints(Min_SL_Pips)) sl_points = PipToPoints(Min_SL_Pips);

  double lots = CalcLots(sl_points);
  if(lots <= 0) return;

  double tp1Price = entry - sl_points * _Point;          // 1R
  double tp2Price = entry - (2.0 * sl_points) * _Point;  // 2R

  trade.Sell(lots, NULL, entry, slPrice, 0.0, "MACD_Stoch_SHORT");
}

//-------------------------------
// Manajemen posisi: partial 80/20, BE, TP2
// Catatan: contoh ini mengilustrasikan logika; implementasi penuh perlu tracking tiket/order.
//-------------------------------
void ManageOpenPositions()
{
  int total = PositionsTotal();
  for(int i=total-1; i>=0; --i)
  {
    ulong ticket = PositionGetTicket(i);
    if(!PositionSelectByTicket(ticket)) continue;
    if(PositionGetInteger(POSITION_MAGIC)!= (long)Magic) continue;

    string sym = PositionGetString(POSITION_SYMBOL);
    long   type = PositionGetInteger(POSITION_TYPE);
    double priceOpen = PositionGetDouble(POSITION_PRICE_OPEN);
    double sl = PositionGetDouble(POSITION_SL);
    double volume = PositionGetDouble(POSITION_VOLUME);

    double bid = SymbolInfoDouble(sym, SYMBOL_BID);
    double ask = SymbolInfoDouble(sym, SYMBOL_ASK);

    // Hitung 1R berdasarkan SL jarak awal
    double r_points = 0.0;
    if(type==POSITION_TYPE_BUY)  r_points = (priceOpen - sl) / _Point;
    if(type==POSITION_TYPE_SELL) r_points = (sl - priceOpen) / _Point;
    if(r_points<=0) continue;

    // TP1 & TP2 target
    double tp1 = 0.0, tp2 = 0.0;
    if(type==POSITION_TYPE_BUY)
    { tp1 = priceOpen + r_points*_Point; tp2 = priceOpen + 2.0*r_points*_Point; }
    else
    { tp1 = priceOpen - r_points*_Point; tp2 = priceOpen - 2.0*r_points*_Point; }

    // Deteksi TP1 tercapai dan belum partial
    // Perlu mekanisme flag; demo sederhana pakai volume sebagai indikator partial.
    bool tp1Hit=false, tp2Hit=false;
    if(type==POSITION_TYPE_BUY)  { tp1Hit = (bid>=tp1); tp2Hit = (bid>=tp2); }
    if(type==POSITION_TYPE_SELL) { tp1Hit = (ask<=tp1); tp2Hit = (ask<=tp2); }

    // TP2: tutup semua sisa
    if(tp2Hit)
    {
      trade.PositionClose(ticket);
      continue;
    }

    // TP1: partial close 80% dan SL -> BE
    if(tp1Hit)
    {
      if(Use_Partial && volume > SymbolInfoDouble(sym, SYMBOL_VOLUME_MIN))
      {
        double volClose = volume * (Partial_Close_Percent/100.0);
        double step = SymbolInfoDouble(sym, SYMBOL_VOLUME_STEP);
        volClose = MathMax(step, MathFloor(volClose/step)*step);
        trade.PositionClosePartial(ticket, volClose);
      }
      // Pindahkan SL ke breakeven
      if(type==POSITION_TYPE_BUY)  trade.PositionModify(ticket, priceOpen, 0.0);
      if(type==POSITION_TYPE_SELL) trade.PositionModify(ticket, priceOpen, 0.0);
    }
  }
}

Penjelasan singkat:

  • Struktur mengikuti event MQL5: OnInit menyiapkan handle indikator, OnTick memproses sinyal hanya saat bar M5 baru, lalu mengevaluasi aturan MACD histogram + Stochastic dan mengeksekusi order setelah candle konfirmasi.
  • Manajemen risiko menerapkan SL dari base candle; target 1R untuk partial 80% lalu SL ke breakeven dan target akhir 2R, sesuai aturan di dokumen.
  • Kerangka menyertakan filter spread dan sesi trading, serta placeholder kalkulasi lot berbasis risiko. Silakan sesuaikan konversi pips→points sesuai simbol yang dipakai.

Catatan implementasi:

  • Untuk produksi, tambahkan tracking state per posisi (custom comment/tag, map ticket→R, status partial) agar logika TP1/TP2 lebih andal.
  • Sesuaikan deteksi “rebound sebelum nol” MACD histogram dengan aturan yang lebih presisi jika diperlukan (mis. deteksi local min/max).