2026-04-15 23:53:01 +07:00
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
|
//| HEDGE FUND ULTIMATE PRO v5.1 – DUAL ENGINE (MT5) |
|
|
|
|
|
|
//| Swing Trend-Follow + Scalp Mean-Reversion |
|
|
|
|
|
|
//| God-Tier: Adaptive Risk + Anti-MC + Profit Accumulator |
|
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
|
#property strict
|
|
|
|
|
|
#property copyright "Hedge Ultimate Pro v5.1"
|
|
|
|
|
|
#property version "5.10"
|
|
|
|
|
|
|
|
|
|
|
|
#include <Trade/Trade.mqh>
|
|
|
|
|
|
CTrade trade;
|
|
|
|
|
|
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
// RISK MANAGEMENT
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
input group "=== Risk Management ==="
|
|
|
|
|
|
input double RiskPercent = 0.75; // Risk per trade swing (% balance)
|
|
|
|
|
|
input double ScalpRiskPercent = 0.3; // Risk per trade scalp (% balance) - kecil!
|
|
|
|
|
|
input double MaxDailyLossP = 3.0; // Max daily loss (% balance)
|
|
|
|
|
|
input double MaxDrawdownP = 8.0; // Max equity drawdown (%) - circuit breaker
|
|
|
|
|
|
input int MaxSwingPos = 2; // Max open swing positions
|
|
|
|
|
|
input int MaxScalpPos = 3; // Max open scalp positions
|
|
|
|
|
|
input int MagicSwing = 778899; // Magic Number swing
|
|
|
|
|
|
input int MagicScalp = 778900; // Magic Number scalp
|
|
|
|
|
|
input int CooldownBars = 3; // Cooldown bars setelah loss
|
|
|
|
|
|
input int MaxConsecLoss = 3; // N loss berturut → risk -50%
|
|
|
|
|
|
input int MaxTotalPos = 4; // Max TOTAL posisi (swing+scalp combined)
|
|
|
|
|
|
input double DailyProfitTarget = 2.0; // Stop trading jika profit hari ini >= % balance (0=off)
|
|
|
|
|
|
input double MinMarginLevel = 200.0; // Min margin level % untuk entry baru
|
|
|
|
|
|
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
// TREND FILTER (SWING)
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
input group "=== Swing: Trend Filter ==="
|
|
|
|
|
|
input int EMA_Fast = 50;
|
|
|
|
|
|
input int EMA_Slow = 200;
|
|
|
|
|
|
input ENUM_TIMEFRAMES HTF = PERIOD_H1; // HTF untuk trend direction
|
|
|
|
|
|
input int ADX_Period = 14;
|
|
|
|
|
|
input double ADX_Min = 20.0;
|
|
|
|
|
|
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
// SWING ENTRY
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
input group "=== Swing: Entry ==="
|
|
|
|
|
|
input int RSI_Period = 14;
|
|
|
|
|
|
input double RSI_OB = 70.0;
|
|
|
|
|
|
input double RSI_OS = 30.0;
|
|
|
|
|
|
input double ATR_SL_Multi = 1.5; // SL = ATR x multi
|
|
|
|
|
|
input double Swing_RR = 2.5; // R:R ratio swing
|
|
|
|
|
|
input int ATR_Period = 14;
|
|
|
|
|
|
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
// SWING TRADE MANAGEMENT
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
input group "=== Swing: Management ==="
|
|
|
|
|
|
input bool UsePartialClose = true;
|
|
|
|
|
|
input double PartialCloseRR = 1.0;
|
|
|
|
|
|
input double PartialPercent = 50.0;
|
|
|
|
|
|
input double BE_ATR_Multi = 1.0;
|
|
|
|
|
|
input double Trail_ATR_Multi = 1.5;
|
|
|
|
|
|
input double Trail_Step_ATR = 0.8;
|
|
|
|
|
|
input bool CloseOnOpposite = true;
|
|
|
|
|
|
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
// SCALP SETTINGS
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
input group "=== Scalp: Settings ==="
|
|
|
|
|
|
input bool EnableScalp = true; // Aktifkan scalp mode
|
|
|
|
|
|
input int BB_Period = 20; // Bollinger Band period
|
|
|
|
|
|
input double BB_Dev = 2.0; // BB deviasi
|
|
|
|
|
|
input int Scalp_RSI_Period = 7; // RSI cepat untuk scalp
|
|
|
|
|
|
input double Scalp_RSI_OB = 75.0; // RSI overbought scalp trigger
|
|
|
|
|
|
input double Scalp_RSI_OS = 25.0; // RSI oversold scalp trigger
|
|
|
|
|
|
input double Scalp_RR = 1.2; // R:R ratio scalp (cepat ambil profit)
|
|
|
|
|
|
input double Scalp_SL_ATR = 0.8; // SL scalp = ATR x multi (ketat)
|
|
|
|
|
|
input double Scalp_MaxSpread = 15; // Max spread untuk scalp (lebih ketat)
|
|
|
|
|
|
input double Scalp_BE_ATR = 0.3; // Scalp BE trigger = ATR × multi (adaptive)
|
|
|
|
|
|
input double Scalp_Trail_ATR = 0.15; // Scalp trail distance = ATR × multi (adaptive)
|
|
|
|
|
|
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
// FILTERS
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
input group "=== Filters ==="
|
|
|
|
|
|
input double MaxSpread = 30;
|
|
|
|
|
|
input bool UseSessionFilter = true;
|
|
|
|
|
|
input int SessionStartHour = 2; // London open
|
|
|
|
|
|
input int SessionEndHour = 20; // NY close
|
|
|
|
|
|
input int ScalpStartHour = 7; // Scalp hanya jam sibuk
|
|
|
|
|
|
input int ScalpEndHour = 17; // Scalp stop sebelum NY close
|
|
|
|
|
|
input bool CloseBeforeWeekend = true;
|
|
|
|
|
|
input bool AvoidHighImpactHour = true; // Skip entry 30m sebelum/sesudah news hour
|
|
|
|
|
|
input string NewsHours = "8,13,15"; // Jam server rawan news (pisah koma)
|
|
|
|
|
|
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
// GLOBAL VARIABLES
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
int ema_fast_handle, ema_slow_handle;
|
|
|
|
|
|
int adx_handle, rsi_handle, atr_handle;
|
|
|
|
|
|
int bb_handle, scalp_rsi_handle;
|
|
|
|
|
|
double ema_fast_val[], ema_slow_val[];
|
|
|
|
|
|
double adx_val[], plus_di[], minus_di[];
|
|
|
|
|
|
double rsi_val[], atr_val[];
|
|
|
|
|
|
double bb_upper[], bb_lower[], bb_mid[];
|
|
|
|
|
|
double scalp_rsi_val[];
|
|
|
|
|
|
|
|
|
|
|
|
datetime lastSwingBar = 0;
|
|
|
|
|
|
datetime lastLossTime = 0;
|
|
|
|
|
|
int consecLosses = 0;
|
|
|
|
|
|
datetime lastTradeDay = 0; // Track hari terakhir untuk OnTrade reset
|
2026-05-25 08:29:56 +07:00
|
|
|
|
int lastDealsTotal = 0; // Track total deal terakhir untuk OnTrade
|
2026-04-15 23:53:01 +07:00
|
|
|
|
|
|
|
|
|
|
string gvPeakEquity;
|
|
|
|
|
|
|
|
|
|
|
|
// News hours parsed
|
|
|
|
|
|
int newsHourArr[10];
|
|
|
|
|
|
int newsHourCount = 0;
|
|
|
|
|
|
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
// AUTO-DETECT FILLING MODE
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
ENUM_ORDER_TYPE_FILLING DetectFilling()
|
|
|
|
|
|
{
|
|
|
|
|
|
long fillMode = 0;
|
|
|
|
|
|
SymbolInfoInteger(_Symbol, SYMBOL_FILLING_MODE, fillMode);
|
|
|
|
|
|
if((fillMode & SYMBOL_FILLING_FOK) != 0) return ORDER_FILLING_FOK;
|
|
|
|
|
|
if((fillMode & SYMBOL_FILLING_IOC) != 0) return ORDER_FILLING_IOC;
|
|
|
|
|
|
return ORDER_FILLING_RETURN;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
// PARSE NEWS HOURS STRING
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
void ParseNewsHours()
|
|
|
|
|
|
{
|
|
|
|
|
|
newsHourCount = 0;
|
|
|
|
|
|
string parts[];
|
|
|
|
|
|
int count = StringSplit(NewsHours, ',', parts);
|
|
|
|
|
|
for(int i = 0; i < count && i < 10; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
StringTrimLeft(parts[i]);
|
|
|
|
|
|
StringTrimRight(parts[i]);
|
|
|
|
|
|
newsHourArr[newsHourCount] = (int)StringToInteger(parts[i]);
|
|
|
|
|
|
newsHourCount++;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool IsNearNewsHour()
|
|
|
|
|
|
{
|
|
|
|
|
|
if(!AvoidHighImpactHour || newsHourCount == 0) return false;
|
|
|
|
|
|
|
|
|
|
|
|
MqlDateTime dt;
|
|
|
|
|
|
TimeCurrent(dt);
|
|
|
|
|
|
int min = dt.hour * 60 + dt.min;
|
|
|
|
|
|
|
|
|
|
|
|
for(int i = 0; i < newsHourCount; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
int newsMin = newsHourArr[i] * 60;
|
|
|
|
|
|
// Hindari 30 menit sebelum dan 15 menit sesudah
|
|
|
|
|
|
if(min >= newsMin - 30 && min <= newsMin + 15)
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
// INIT
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
int OnInit()
|
|
|
|
|
|
{
|
|
|
|
|
|
trade.SetExpertMagicNumber(MagicSwing);
|
|
|
|
|
|
trade.SetDeviationInPoints(10);
|
|
|
|
|
|
trade.SetTypeFilling(DetectFilling());
|
|
|
|
|
|
|
|
|
|
|
|
// Swing indicators
|
|
|
|
|
|
ema_fast_handle = iMA(_Symbol, HTF, EMA_Fast, 0, MODE_EMA, PRICE_CLOSE);
|
|
|
|
|
|
ema_slow_handle = iMA(_Symbol, HTF, EMA_Slow, 0, MODE_EMA, PRICE_CLOSE);
|
|
|
|
|
|
adx_handle = iADX(_Symbol, _Period, ADX_Period);
|
|
|
|
|
|
rsi_handle = iRSI(_Symbol, _Period, RSI_Period, PRICE_CLOSE);
|
|
|
|
|
|
atr_handle = iATR(_Symbol, _Period, ATR_Period);
|
|
|
|
|
|
|
|
|
|
|
|
// Scalp indicators
|
|
|
|
|
|
bb_handle = iBands(_Symbol, _Period, BB_Period, 0, BB_Dev, PRICE_CLOSE);
|
|
|
|
|
|
scalp_rsi_handle = iRSI(_Symbol, _Period, Scalp_RSI_Period, PRICE_CLOSE);
|
|
|
|
|
|
|
|
|
|
|
|
if(ema_fast_handle==INVALID_HANDLE || ema_slow_handle==INVALID_HANDLE ||
|
|
|
|
|
|
adx_handle==INVALID_HANDLE || rsi_handle==INVALID_HANDLE ||
|
|
|
|
|
|
atr_handle==INVALID_HANDLE || bb_handle==INVALID_HANDLE ||
|
|
|
|
|
|
scalp_rsi_handle==INVALID_HANDLE)
|
|
|
|
|
|
{
|
|
|
|
|
|
Print("ERROR: Gagal membuat indicator handle!");
|
|
|
|
|
|
return(INIT_FAILED);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ArraySetAsSeries(ema_fast_val, true);
|
|
|
|
|
|
ArraySetAsSeries(ema_slow_val, true);
|
|
|
|
|
|
ArraySetAsSeries(adx_val, true);
|
|
|
|
|
|
ArraySetAsSeries(plus_di, true);
|
|
|
|
|
|
ArraySetAsSeries(minus_di, true);
|
|
|
|
|
|
ArraySetAsSeries(rsi_val, true);
|
|
|
|
|
|
ArraySetAsSeries(atr_val, true);
|
|
|
|
|
|
ArraySetAsSeries(bb_upper, true);
|
|
|
|
|
|
ArraySetAsSeries(bb_lower, true);
|
|
|
|
|
|
ArraySetAsSeries(bb_mid, true);
|
|
|
|
|
|
ArraySetAsSeries(scalp_rsi_val, true);
|
|
|
|
|
|
|
|
|
|
|
|
// Peak equity persistent
|
|
|
|
|
|
gvPeakEquity = "HUP5_PeakEq_" + _Symbol + "_" + IntegerToString(MagicSwing);
|
|
|
|
|
|
if(GlobalVariableCheck(gvPeakEquity))
|
|
|
|
|
|
{
|
|
|
|
|
|
double stored = GlobalVariableGet(gvPeakEquity);
|
|
|
|
|
|
double current = AccountInfoDouble(ACCOUNT_EQUITY);
|
|
|
|
|
|
if(stored > current && stored < current * 2.0)
|
|
|
|
|
|
current = stored;
|
|
|
|
|
|
GlobalVariableSet(gvPeakEquity, current);
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
GlobalVariableSet(gvPeakEquity, AccountInfoDouble(ACCOUNT_EQUITY));
|
|
|
|
|
|
|
|
|
|
|
|
CountRecentLosses();
|
|
|
|
|
|
ParseNewsHours();
|
|
|
|
|
|
|
2026-05-25 08:29:56 +07:00
|
|
|
|
// Inisialisasi tracking bar dan trade history untuk mencegah double-counting & entry tengah bar saat startup
|
|
|
|
|
|
lastSwingBar = iTime(_Symbol, _Period, 0);
|
|
|
|
|
|
lastTradeDay = iTime(_Symbol, PERIOD_D1, 0);
|
|
|
|
|
|
HistorySelect(lastTradeDay, TimeCurrent());
|
|
|
|
|
|
lastDealsTotal = HistoryDealsTotal();
|
|
|
|
|
|
|
2026-04-15 23:53:01 +07:00
|
|
|
|
Print("HUP v5.0 DUAL ENGINE | Filling=", EnumToString(DetectFilling()),
|
|
|
|
|
|
" | Scalp=", EnableScalp ? "ON" : "OFF",
|
|
|
|
|
|
" | ConsecLoss=", consecLosses);
|
|
|
|
|
|
|
|
|
|
|
|
return(INIT_SUCCEEDED);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void OnDeinit(const int reason)
|
|
|
|
|
|
{
|
|
|
|
|
|
if(ema_fast_handle!=INVALID_HANDLE) IndicatorRelease(ema_fast_handle);
|
|
|
|
|
|
if(ema_slow_handle!=INVALID_HANDLE) IndicatorRelease(ema_slow_handle);
|
|
|
|
|
|
if(adx_handle!=INVALID_HANDLE) IndicatorRelease(adx_handle);
|
|
|
|
|
|
if(rsi_handle!=INVALID_HANDLE) IndicatorRelease(rsi_handle);
|
|
|
|
|
|
if(atr_handle!=INVALID_HANDLE) IndicatorRelease(atr_handle);
|
|
|
|
|
|
if(bb_handle!=INVALID_HANDLE) IndicatorRelease(bb_handle);
|
|
|
|
|
|
if(scalp_rsi_handle!=INVALID_HANDLE) IndicatorRelease(scalp_rsi_handle);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
// COUNT RECENT CONSECUTIVE LOSSES
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
void CountRecentLosses()
|
|
|
|
|
|
{
|
|
|
|
|
|
consecLosses = 0;
|
2026-05-25 08:29:56 +07:00
|
|
|
|
// Scan history 30 hari ke belakang untuk consec losses lintas hari
|
|
|
|
|
|
HistorySelect(TimeCurrent() - 30 * 24 * 3600, TimeCurrent());
|
2026-04-15 23:53:01 +07:00
|
|
|
|
|
|
|
|
|
|
int total = HistoryDealsTotal();
|
|
|
|
|
|
for(int i = total - 1; i >= 0; i--)
|
|
|
|
|
|
{
|
|
|
|
|
|
ulong ticket = HistoryDealGetTicket(i);
|
|
|
|
|
|
if(ticket == 0) continue;
|
|
|
|
|
|
long magic = (long)HistoryDealGetInteger(ticket, DEAL_MAGIC);
|
|
|
|
|
|
if(magic != MagicSwing && magic != MagicScalp) continue;
|
|
|
|
|
|
if(HistoryDealGetString(ticket, DEAL_SYMBOL) != _Symbol) continue;
|
|
|
|
|
|
if(HistoryDealGetInteger(ticket, DEAL_ENTRY) != DEAL_ENTRY_OUT) continue;
|
|
|
|
|
|
|
|
|
|
|
|
double profit = HistoryDealGetDouble(ticket, DEAL_PROFIT)
|
|
|
|
|
|
+ HistoryDealGetDouble(ticket, DEAL_SWAP)
|
|
|
|
|
|
+ HistoryDealGetDouble(ticket, DEAL_COMMISSION);
|
|
|
|
|
|
|
|
|
|
|
|
if(profit < 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
consecLosses++;
|
|
|
|
|
|
if(consecLosses == 1)
|
|
|
|
|
|
lastLossTime = (datetime)HistoryDealGetInteger(ticket, DEAL_TIME);
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
// COUNT POSITIONS (by magic)
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
int CountPositions(int magic)
|
|
|
|
|
|
{
|
|
|
|
|
|
int count = 0;
|
|
|
|
|
|
for(int i = PositionsTotal()-1; i >= 0; i--)
|
|
|
|
|
|
{
|
|
|
|
|
|
ulong ticket = PositionGetTicket(i);
|
|
|
|
|
|
if(!PositionSelectByTicket(ticket)) continue;
|
|
|
|
|
|
if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
|
|
|
|
|
|
if(PositionGetInteger(POSITION_MAGIC) != magic) continue;
|
|
|
|
|
|
count++;
|
|
|
|
|
|
}
|
|
|
|
|
|
return count;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
// CLOSE POSITIONS (by magic + optional direction filter)
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
void ClosePositions(int magic, int dirFilter=0)
|
|
|
|
|
|
{
|
|
|
|
|
|
for(int i = PositionsTotal()-1; i >= 0; i--)
|
|
|
|
|
|
{
|
|
|
|
|
|
ulong ticket = PositionGetTicket(i);
|
|
|
|
|
|
if(!PositionSelectByTicket(ticket)) continue;
|
|
|
|
|
|
if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
|
|
|
|
|
|
if(PositionGetInteger(POSITION_MAGIC) != magic) continue;
|
|
|
|
|
|
|
|
|
|
|
|
if(dirFilter != 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
int type = (int)PositionGetInteger(POSITION_TYPE);
|
|
|
|
|
|
if(dirFilter == +1 && type != POSITION_TYPE_SELL) continue; // close sells only
|
|
|
|
|
|
if(dirFilter == -1 && type != POSITION_TYPE_BUY) continue; // close buys only
|
|
|
|
|
|
}
|
|
|
|
|
|
trade.SetExpertMagicNumber(magic);
|
|
|
|
|
|
trade.PositionClose(ticket);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void CloseAllMyPositions()
|
|
|
|
|
|
{
|
|
|
|
|
|
ClosePositions(MagicSwing);
|
|
|
|
|
|
ClosePositions(MagicScalp);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
// DAILY LOSS CHECK (both engines combined)
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
double GetTodayPnL()
|
|
|
|
|
|
{
|
|
|
|
|
|
double pnl = 0;
|
|
|
|
|
|
datetime dayStart = iTime(_Symbol, PERIOD_D1, 0);
|
|
|
|
|
|
HistorySelect(dayStart, TimeCurrent());
|
|
|
|
|
|
|
|
|
|
|
|
for(int i = 0; i < HistoryDealsTotal(); i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
ulong ticket = HistoryDealGetTicket(i);
|
|
|
|
|
|
if(ticket == 0) continue;
|
|
|
|
|
|
long magic = (long)HistoryDealGetInteger(ticket, DEAL_MAGIC);
|
|
|
|
|
|
if(magic != MagicSwing && magic != MagicScalp) continue;
|
|
|
|
|
|
if(HistoryDealGetString(ticket, DEAL_SYMBOL) != _Symbol) continue;
|
|
|
|
|
|
|
|
|
|
|
|
pnl += HistoryDealGetDouble(ticket, DEAL_PROFIT)
|
|
|
|
|
|
+ HistoryDealGetDouble(ticket, DEAL_SWAP)
|
|
|
|
|
|
+ HistoryDealGetDouble(ticket, DEAL_COMMISSION);
|
|
|
|
|
|
}
|
|
|
|
|
|
return pnl;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool DailySafe()
|
|
|
|
|
|
{
|
|
|
|
|
|
double balance = AccountInfoDouble(ACCOUNT_BALANCE);
|
|
|
|
|
|
double maxLoss = balance * MaxDailyLossP / 100.0;
|
|
|
|
|
|
double closedPnL = GetTodayPnL();
|
|
|
|
|
|
|
|
|
|
|
|
// Cek closed PnL
|
|
|
|
|
|
if(closedPnL < -maxLoss)
|
|
|
|
|
|
{
|
|
|
|
|
|
CloseAllMyPositions();
|
|
|
|
|
|
Print("DAILY LOSS LIMIT: closed PnL=", DoubleToString(closedPnL,2));
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Cek closed + floating PnL combined
|
|
|
|
|
|
double floatingPnL = GetFloatingPnL();
|
|
|
|
|
|
if((closedPnL + floatingPnL) < -maxLoss)
|
|
|
|
|
|
{
|
|
|
|
|
|
CloseAllMyPositions();
|
|
|
|
|
|
Print("DAILY LOSS LIMIT (floating): total=", DoubleToString(closedPnL+floatingPnL,2));
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
// FLOATING PnL (posisi terbuka milik EA ini)
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
double GetFloatingPnL()
|
|
|
|
|
|
{
|
|
|
|
|
|
double pnl = 0;
|
|
|
|
|
|
for(int i = PositionsTotal()-1; i >= 0; i--)
|
|
|
|
|
|
{
|
|
|
|
|
|
ulong ticket = PositionGetTicket(i);
|
|
|
|
|
|
if(!PositionSelectByTicket(ticket)) continue;
|
|
|
|
|
|
if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
|
|
|
|
|
|
long magic = (long)PositionGetInteger(POSITION_MAGIC);
|
|
|
|
|
|
if(magic != MagicSwing && magic != MagicScalp) continue;
|
|
|
|
|
|
pnl += PositionGetDouble(POSITION_PROFIT)
|
|
|
|
|
|
+ PositionGetDouble(POSITION_SWAP);
|
|
|
|
|
|
}
|
|
|
|
|
|
return pnl;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
// DAILY PROFIT TARGET (stop trading kalau sudah cukup)
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
bool DailyTargetReached()
|
|
|
|
|
|
{
|
|
|
|
|
|
if(DailyProfitTarget <= 0) return false;
|
|
|
|
|
|
double balance = AccountInfoDouble(ACCOUNT_BALANCE);
|
|
|
|
|
|
double target = balance * DailyProfitTarget / 100.0;
|
|
|
|
|
|
double todayPnL = GetTodayPnL();
|
|
|
|
|
|
return (todayPnL >= target);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
// MARGIN CHECK
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
bool MarginOK()
|
|
|
|
|
|
{
|
|
|
|
|
|
double marginLevel = AccountInfoDouble(ACCOUNT_MARGIN_LEVEL);
|
|
|
|
|
|
// marginLevel=0 berarti tidak ada posisi terbuka (unlimited)
|
|
|
|
|
|
if(marginLevel == 0) return true;
|
|
|
|
|
|
return (marginLevel >= MinMarginLevel);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
// TOTAL POSITION CHECK (combined)
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
int CountAllMyPositions()
|
|
|
|
|
|
{
|
|
|
|
|
|
int count = 0;
|
|
|
|
|
|
for(int i = PositionsTotal()-1; i >= 0; i--)
|
|
|
|
|
|
{
|
|
|
|
|
|
ulong ticket = PositionGetTicket(i);
|
|
|
|
|
|
if(!PositionSelectByTicket(ticket)) continue;
|
|
|
|
|
|
if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
|
|
|
|
|
|
long magic = (long)PositionGetInteger(POSITION_MAGIC);
|
|
|
|
|
|
if(magic == MagicSwing || magic == MagicScalp)
|
|
|
|
|
|
count++;
|
|
|
|
|
|
}
|
|
|
|
|
|
return count;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
// EQUITY CIRCUIT BREAKER
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
bool EquitySafe()
|
|
|
|
|
|
{
|
|
|
|
|
|
double equity = AccountInfoDouble(ACCOUNT_EQUITY);
|
|
|
|
|
|
double peak = GlobalVariableGet(gvPeakEquity);
|
|
|
|
|
|
if(peak <= 0) peak = equity;
|
|
|
|
|
|
|
|
|
|
|
|
if(equity > peak)
|
|
|
|
|
|
{
|
|
|
|
|
|
peak = equity;
|
|
|
|
|
|
GlobalVariableSet(gvPeakEquity, peak);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
double ddPercent = (peak - equity) / peak * 100.0;
|
|
|
|
|
|
if(ddPercent >= MaxDrawdownP)
|
|
|
|
|
|
{
|
|
|
|
|
|
CloseAllMyPositions();
|
|
|
|
|
|
Print("CIRCUIT BREAKER: DD ", DoubleToString(ddPercent,1), "%");
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
// FILTERS
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
bool SpreadOK(double maxSpr)
|
|
|
|
|
|
{
|
|
|
|
|
|
double s = (SymbolInfoDouble(_Symbol,SYMBOL_ASK) - SymbolInfoDouble(_Symbol,SYMBOL_BID)) / _Point;
|
|
|
|
|
|
return s <= maxSpr;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool SessionOK()
|
|
|
|
|
|
{
|
|
|
|
|
|
if(!UseSessionFilter) return true;
|
|
|
|
|
|
MqlDateTime dt;
|
|
|
|
|
|
TimeCurrent(dt);
|
|
|
|
|
|
return (dt.hour >= SessionStartHour && dt.hour < SessionEndHour);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool ScalpSessionOK()
|
|
|
|
|
|
{
|
|
|
|
|
|
MqlDateTime dt;
|
|
|
|
|
|
TimeCurrent(dt);
|
|
|
|
|
|
return (dt.hour >= ScalpStartHour && dt.hour < ScalpEndHour);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void CheckWeekendClose()
|
|
|
|
|
|
{
|
|
|
|
|
|
if(!CloseBeforeWeekend) return;
|
|
|
|
|
|
MqlDateTime dt;
|
|
|
|
|
|
TimeCurrent(dt);
|
|
|
|
|
|
if(dt.day_of_week == 5 && dt.hour >= 20)
|
|
|
|
|
|
CloseAllMyPositions();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool CooldownOK()
|
|
|
|
|
|
{
|
|
|
|
|
|
if(CooldownBars <= 0 || lastLossTime == 0) return true;
|
|
|
|
|
|
int barsSinceLoss = iBarShift(_Symbol, _Period, lastLossTime, false);
|
|
|
|
|
|
return (barsSinceLoss >= CooldownBars);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
// DYNAMIC RISK
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
double GetEffectiveRisk(double baseRisk)
|
|
|
|
|
|
{
|
|
|
|
|
|
double risk = baseRisk;
|
|
|
|
|
|
if(consecLosses >= MaxConsecLoss)
|
|
|
|
|
|
risk *= 0.5;
|
|
|
|
|
|
return risk;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
// LOAD ALL INDICATORS
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
bool LoadIndicators()
|
|
|
|
|
|
{
|
|
|
|
|
|
if(CopyBuffer(ema_fast_handle, 0, 0, 3, ema_fast_val) < 3) return false;
|
|
|
|
|
|
if(CopyBuffer(ema_slow_handle, 0, 0, 3, ema_slow_val) < 3) return false;
|
|
|
|
|
|
if(CopyBuffer(adx_handle, 0, 0, 3, adx_val) < 3) return false;
|
|
|
|
|
|
if(CopyBuffer(adx_handle, 1, 0, 3, plus_di) < 3) return false;
|
|
|
|
|
|
if(CopyBuffer(adx_handle, 2, 0, 3, minus_di) < 3) return false;
|
|
|
|
|
|
if(CopyBuffer(rsi_handle, 0, 0, 3, rsi_val) < 3) return false;
|
2026-05-25 08:29:56 +07:00
|
|
|
|
if(CopyBuffer(atr_handle, 0, 0, 50, atr_val) < 50) return false;
|
2026-04-15 23:53:01 +07:00
|
|
|
|
if(CopyBuffer(bb_handle, 1, 0, 3, bb_upper) < 3) return false; // Upper
|
|
|
|
|
|
if(CopyBuffer(bb_handle, 2, 0, 3, bb_lower) < 3) return false; // Lower
|
|
|
|
|
|
if(CopyBuffer(bb_handle, 0, 0, 3, bb_mid) < 3) return false; // Middle
|
|
|
|
|
|
if(CopyBuffer(scalp_rsi_handle, 0, 0, 3, scalp_rsi_val)< 3) return false;
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-25 08:29:56 +07:00
|
|
|
|
//============================================================
|
|
|
|
|
|
// VOLATILITY FILTER (DYNAMIC ATR)
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
bool IsVolatilityOk()
|
|
|
|
|
|
{
|
|
|
|
|
|
int totalATR = ArraySize(atr_val);
|
|
|
|
|
|
if(totalATR < 10) return true; // fallback jika data kurang
|
|
|
|
|
|
double sum = 0;
|
|
|
|
|
|
for(int i = 0; i < totalATR; i++) sum += atr_val[i];
|
|
|
|
|
|
double avgATR = sum / totalATR;
|
|
|
|
|
|
// Volatilitas OK jika ATR saat ini >= 60% dari rata-rata 50 bar terakhir
|
|
|
|
|
|
return (atr_val[1] >= avgATR * 0.6);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-15 23:53:01 +07:00
|
|
|
|
//============================================================
|
|
|
|
|
|
// LOT CALCULATION
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
double LotCalc(double sl_points, double baseRisk)
|
|
|
|
|
|
{
|
|
|
|
|
|
double balance = AccountInfoDouble(ACCOUNT_BALANCE);
|
|
|
|
|
|
double risk = balance * GetEffectiveRisk(baseRisk) / 100.0;
|
|
|
|
|
|
|
|
|
|
|
|
double tickvalue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
|
|
|
|
|
|
double ticksize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
|
|
|
|
|
|
|
|
|
|
|
|
if(sl_points <= 0 || tickvalue <= 0 || ticksize <= 0)
|
|
|
|
|
|
return 0.0;
|
|
|
|
|
|
|
|
|
|
|
|
double lot = risk / (sl_points * (tickvalue / ticksize) * _Point);
|
|
|
|
|
|
|
|
|
|
|
|
double minLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
|
|
|
|
|
|
double maxLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
|
|
|
|
|
|
double lotStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
|
|
|
|
|
|
if(lotStep <= 0) lotStep = 0.01;
|
|
|
|
|
|
|
|
|
|
|
|
lot = MathFloor(lot / lotStep) * lotStep;
|
|
|
|
|
|
lot = MathMax(minLot, MathMin(maxLot, lot));
|
|
|
|
|
|
return NormalizeDouble(lot, 2);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
// VALIDATE SL/TP vs BROKER LIMITS
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
bool ValidateSLTP(double price, double sl, double tp, int direction)
|
|
|
|
|
|
{
|
|
|
|
|
|
long stopsLevel = 0;
|
|
|
|
|
|
SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL, stopsLevel);
|
|
|
|
|
|
double minDist = stopsLevel * _Point;
|
|
|
|
|
|
|
|
|
|
|
|
if(minDist > 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
if(direction == +1)
|
|
|
|
|
|
{
|
2026-05-25 08:29:56 +07:00
|
|
|
|
if((sl > 0 && (price - sl) < minDist) || (tp > 0 && (tp - price) < minDist)) return false;
|
2026-04-15 23:53:01 +07:00
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
2026-05-25 08:29:56 +07:00
|
|
|
|
if((sl > 0 && (sl - price) < minDist) || (tp > 0 && (price - tp) < minDist)) return false;
|
2026-04-15 23:53:01 +07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
long freezeLevel = 0;
|
|
|
|
|
|
SymbolInfoInteger(_Symbol, SYMBOL_TRADE_FREEZE_LEVEL, freezeLevel);
|
|
|
|
|
|
double freezeDist = freezeLevel * _Point;
|
|
|
|
|
|
if(freezeDist > 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
|
|
|
|
|
|
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
|
2026-05-25 08:29:56 +07:00
|
|
|
|
if(direction == +1 && sl > 0 && (ask - sl) < freezeDist) return false;
|
|
|
|
|
|
if(direction == -1 && sl > 0 && (sl - bid) < freezeDist) return false;
|
2026-04-15 23:53:01 +07:00
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
double NormalizePrice(double price)
|
|
|
|
|
|
{
|
|
|
|
|
|
double tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
|
|
|
|
|
|
if(tickSize <= 0) return NormalizeDouble(price, _Digits);
|
|
|
|
|
|
return NormalizeDouble(MathRound(price / tickSize) * tickSize, _Digits);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
// TREND ANALYSIS (SWING)
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
int GetTrend()
|
|
|
|
|
|
{
|
|
|
|
|
|
if(ema_fast_val[0] > ema_slow_val[0]) return +1;
|
|
|
|
|
|
if(ema_fast_val[0] < ema_slow_val[0]) return -1;
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
bool TrendStrong() { return adx_val[0] >= ADX_Min; }
|
|
|
|
|
|
|
2026-05-25 08:29:56 +07:00
|
|
|
|
//============================================================
|
|
|
|
|
|
// DETECT PIN BAR PATTERN
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
bool IsPinBar(int barIndex, int direction)
|
|
|
|
|
|
{
|
|
|
|
|
|
double open = iOpen(_Symbol, _Period, barIndex);
|
|
|
|
|
|
double close = iClose(_Symbol, _Period, barIndex);
|
|
|
|
|
|
double high = iHigh(_Symbol, _Period, barIndex);
|
|
|
|
|
|
double low = iLow(_Symbol, _Period, barIndex);
|
|
|
|
|
|
|
|
|
|
|
|
double range = high - low;
|
|
|
|
|
|
if(range <= 0) return false;
|
|
|
|
|
|
|
|
|
|
|
|
double body = MathAbs(close - open);
|
|
|
|
|
|
|
|
|
|
|
|
if(direction == +1) // Bullish Pin Bar (Hammer)
|
|
|
|
|
|
{
|
|
|
|
|
|
double lowerShadow = MathMin(open, close) - low;
|
|
|
|
|
|
double upperShadow = high - MathMax(open, close);
|
|
|
|
|
|
return (lowerShadow >= range * 0.6 && body <= range * 0.3 && upperShadow <= range * 0.2);
|
|
|
|
|
|
}
|
|
|
|
|
|
else if(direction == -1) // Bearish Pin Bar (Shooting Star)
|
|
|
|
|
|
{
|
|
|
|
|
|
double lowerShadow = MathMin(open, close) - low;
|
|
|
|
|
|
double upperShadow = high - MathMax(open, close);
|
|
|
|
|
|
return (upperShadow >= range * 0.6 && body <= range * 0.3 && lowerShadow <= range * 0.2);
|
|
|
|
|
|
}
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-15 23:53:01 +07:00
|
|
|
|
//============================================================
|
|
|
|
|
|
// SWING BUY SIGNAL
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
bool SwingBuySignal()
|
|
|
|
|
|
{
|
|
|
|
|
|
if(GetTrend() != +1) return false;
|
|
|
|
|
|
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
|
|
|
|
|
|
if(bid <= ema_fast_val[0]) return false;
|
|
|
|
|
|
|
|
|
|
|
|
if(!TrendStrong()) return false;
|
|
|
|
|
|
if(plus_di[0] <= minus_di[0]) return false;
|
|
|
|
|
|
|
2026-05-25 08:29:56 +07:00
|
|
|
|
// Dioptimalkan menjadi 35.0 (sebelumnya 40.0) agar bisa menangkap pullback sehat yang lebih dalam
|
|
|
|
|
|
if(rsi_val[0] >= RSI_OB || rsi_val[0] < 35.0) return false;
|
2026-04-15 23:53:01 +07:00
|
|
|
|
|
|
|
|
|
|
double open1 = iOpen(_Symbol, _Period, 1);
|
|
|
|
|
|
double close1 = iClose(_Symbol, _Period, 1);
|
|
|
|
|
|
double open2 = iOpen(_Symbol, _Period, 2);
|
|
|
|
|
|
double close2 = iClose(_Symbol, _Period, 2);
|
|
|
|
|
|
|
|
|
|
|
|
double body1 = close1 - open1;
|
|
|
|
|
|
double body2 = close2 - open2;
|
|
|
|
|
|
double range1 = iHigh(_Symbol, _Period, 1) - iLow(_Symbol, _Period, 1);
|
|
|
|
|
|
|
|
|
|
|
|
double atr = atr_val[1];
|
|
|
|
|
|
if(atr <= 0) return false;
|
|
|
|
|
|
|
|
|
|
|
|
if(body1 <= 0 || body1 < atr * 0.4) return false;
|
|
|
|
|
|
if(range1 > 0 && body1 / range1 < 0.5) return false;
|
|
|
|
|
|
|
|
|
|
|
|
bool breakout = close1 > iHigh(_Symbol, _Period, 2);
|
|
|
|
|
|
bool engulfing = (body2 < 0) && (close1 > open2) && (open1 < close2);
|
2026-05-25 08:29:56 +07:00
|
|
|
|
bool pinbar = IsPinBar(1, +1);
|
|
|
|
|
|
return (breakout || engulfing || pinbar);
|
2026-04-15 23:53:01 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
// SWING SELL SIGNAL
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
bool SwingSellSignal()
|
|
|
|
|
|
{
|
|
|
|
|
|
if(GetTrend() != -1) return false;
|
|
|
|
|
|
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
|
|
|
|
|
|
if(ask >= ema_fast_val[0]) return false;
|
|
|
|
|
|
|
|
|
|
|
|
if(!TrendStrong()) return false;
|
|
|
|
|
|
if(minus_di[0] <= plus_di[0]) return false;
|
|
|
|
|
|
|
2026-05-25 08:29:56 +07:00
|
|
|
|
// Dioptimalkan menjadi 65.0 (sebelumnya 60.0) agar bisa menangkap pullback sehat yang lebih tinggi
|
|
|
|
|
|
if(rsi_val[0] <= RSI_OS || rsi_val[0] > 65.0) return false;
|
2026-04-15 23:53:01 +07:00
|
|
|
|
|
|
|
|
|
|
double open1 = iOpen(_Symbol, _Period, 1);
|
|
|
|
|
|
double close1 = iClose(_Symbol, _Period, 1);
|
|
|
|
|
|
double open2 = iOpen(_Symbol, _Period, 2);
|
|
|
|
|
|
double close2 = iClose(_Symbol, _Period, 2);
|
|
|
|
|
|
|
|
|
|
|
|
double body1 = open1 - close1;
|
|
|
|
|
|
double body2 = open2 - close2;
|
|
|
|
|
|
double range1 = iHigh(_Symbol, _Period, 1) - iLow(_Symbol, _Period, 1);
|
|
|
|
|
|
|
|
|
|
|
|
double atr = atr_val[1];
|
|
|
|
|
|
if(atr <= 0) return false;
|
|
|
|
|
|
|
|
|
|
|
|
if(body1 <= 0 || body1 < atr * 0.4) return false;
|
|
|
|
|
|
if(range1 > 0 && body1 / range1 < 0.5) return false;
|
|
|
|
|
|
|
|
|
|
|
|
bool breakout = close1 < iLow(_Symbol, _Period, 2);
|
|
|
|
|
|
bool engulfing = (body2 < 0) && (close1 < open2) && (open1 > close2);
|
2026-05-25 08:29:56 +07:00
|
|
|
|
bool pinbar = IsPinBar(1, -1);
|
|
|
|
|
|
return (breakout || engulfing || pinbar);
|
2026-04-15 23:53:01 +07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
// SCALP BUY SIGNAL (BB Lower bounce + RSI oversold)
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
bool ScalpBuySignal()
|
|
|
|
|
|
{
|
|
|
|
|
|
if(!EnableScalp) return false;
|
|
|
|
|
|
|
|
|
|
|
|
double close1 = iClose(_Symbol, _Period, 1);
|
|
|
|
|
|
double low1 = iLow(_Symbol, _Period, 1);
|
|
|
|
|
|
double close2 = iClose(_Symbol, _Period, 2);
|
|
|
|
|
|
|
|
|
|
|
|
// Kondisi 1: Candle sebelumnya menyentuh/menembus BB lower
|
|
|
|
|
|
if(low1 > bb_lower[1]) return false;
|
|
|
|
|
|
|
|
|
|
|
|
// Kondisi 2: Close kembali di atas BB lower (bounce, bukan tembus terus)
|
|
|
|
|
|
if(close1 < bb_lower[1]) return false;
|
|
|
|
|
|
|
|
|
|
|
|
// Kondisi 3: RSI cepat di area oversold
|
|
|
|
|
|
if(scalp_rsi_val[1] > Scalp_RSI_OS) return false;
|
|
|
|
|
|
|
|
|
|
|
|
// Kondisi 4: Candle bounce bullish (close > open)
|
|
|
|
|
|
if(close1 <= iOpen(_Symbol, _Period, 1)) return false;
|
|
|
|
|
|
|
|
|
|
|
|
// Kondisi 5: Harga di bawah BB middle (masih ada ruang ke atas)
|
|
|
|
|
|
if(close1 > bb_mid[0]) return false;
|
|
|
|
|
|
|
|
|
|
|
|
// Kondisi 6: Tidak melawan trend kuat (ADX > 30 + bearish = skip)
|
|
|
|
|
|
if(adx_val[0] > 30.0 && GetTrend() == -1) return false;
|
|
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
// SCALP SELL SIGNAL (BB Upper bounce + RSI overbought)
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
bool ScalpSellSignal()
|
|
|
|
|
|
{
|
|
|
|
|
|
if(!EnableScalp) return false;
|
|
|
|
|
|
|
|
|
|
|
|
double close1 = iClose(_Symbol, _Period, 1);
|
|
|
|
|
|
double high1 = iHigh(_Symbol, _Period, 1);
|
|
|
|
|
|
double close2 = iClose(_Symbol, _Period, 2);
|
|
|
|
|
|
|
|
|
|
|
|
// Kondisi 1: Candle sebelumnya menyentuh/menembus BB upper
|
|
|
|
|
|
if(high1 < bb_upper[1]) return false;
|
|
|
|
|
|
|
|
|
|
|
|
// Kondisi 2: Close kembali di bawah BB upper (bounce)
|
|
|
|
|
|
if(close1 > bb_upper[1]) return false;
|
|
|
|
|
|
|
|
|
|
|
|
// Kondisi 3: RSI cepat di area overbought
|
|
|
|
|
|
if(scalp_rsi_val[1] < Scalp_RSI_OB) return false;
|
|
|
|
|
|
|
|
|
|
|
|
// Kondisi 4: Candle bounce bearish
|
|
|
|
|
|
if(close1 >= iOpen(_Symbol, _Period, 1)) return false;
|
|
|
|
|
|
|
|
|
|
|
|
// Kondisi 5: Harga di atas BB middle
|
|
|
|
|
|
if(close1 < bb_mid[0]) return false;
|
|
|
|
|
|
|
|
|
|
|
|
// Kondisi 6: Tidak melawan trend kuat
|
|
|
|
|
|
if(adx_val[0] > 30.0 && GetTrend() == +1) return false;
|
|
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
// MANAGE SWING POSITIONS
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
void ManageSwing()
|
|
|
|
|
|
{
|
|
|
|
|
|
double atrNow[];
|
|
|
|
|
|
ArraySetAsSeries(atrNow, true);
|
|
|
|
|
|
if(CopyBuffer(atr_handle, 0, 0, 1, atrNow) < 1) return;
|
|
|
|
|
|
double atr = atrNow[0];
|
|
|
|
|
|
if(atr <= 0) return;
|
|
|
|
|
|
|
|
|
|
|
|
for(int i = PositionsTotal()-1; i >= 0; i--)
|
|
|
|
|
|
{
|
|
|
|
|
|
ulong ticket = PositionGetTicket(i);
|
|
|
|
|
|
if(!PositionSelectByTicket(ticket)) continue;
|
|
|
|
|
|
if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
|
|
|
|
|
|
if(PositionGetInteger(POSITION_MAGIC) != MagicSwing) continue;
|
|
|
|
|
|
|
|
|
|
|
|
double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);
|
|
|
|
|
|
double sl = PositionGetDouble(POSITION_SL);
|
|
|
|
|
|
double tp = PositionGetDouble(POSITION_TP);
|
|
|
|
|
|
double volume = PositionGetDouble(POSITION_VOLUME);
|
|
|
|
|
|
int type = (int)PositionGetInteger(POSITION_TYPE);
|
|
|
|
|
|
|
|
|
|
|
|
double price = (type==POSITION_TYPE_BUY) ?
|
|
|
|
|
|
SymbolInfoDouble(_Symbol,SYMBOL_BID) :
|
|
|
|
|
|
SymbolInfoDouble(_Symbol,SYMBOL_ASK);
|
|
|
|
|
|
|
|
|
|
|
|
double profitDist = (type==POSITION_TYPE_BUY) ?
|
|
|
|
|
|
(price-openPrice) : (openPrice-price);
|
|
|
|
|
|
double slDist = (type==POSITION_TYPE_BUY) ?
|
|
|
|
|
|
(openPrice-sl) : (sl-openPrice);
|
|
|
|
|
|
|
|
|
|
|
|
// Partial Close
|
|
|
|
|
|
if(UsePartialClose && slDist > 0 && profitDist >= slDist * PartialCloseRR)
|
|
|
|
|
|
{
|
|
|
|
|
|
string gvP = "HUP5_SwP_" + IntegerToString(ticket);
|
|
|
|
|
|
if(!GlobalVariableCheck(gvP))
|
|
|
|
|
|
{
|
|
|
|
|
|
double lotStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
|
|
|
|
|
|
if(lotStep <= 0) lotStep = 0.01;
|
|
|
|
|
|
double closeLot = MathFloor((volume * PartialPercent / 100.0) / lotStep) * lotStep;
|
|
|
|
|
|
closeLot = NormalizeDouble(closeLot, 2);
|
|
|
|
|
|
double minLot = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
|
|
|
|
|
|
if(closeLot >= minLot && (volume - closeLot) >= minLot)
|
|
|
|
|
|
{
|
|
|
|
|
|
trade.SetExpertMagicNumber(MagicSwing);
|
|
|
|
|
|
if(trade.PositionClosePartial(ticket, closeLot))
|
|
|
|
|
|
GlobalVariableSet(gvP, 1.0);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Breakeven
|
|
|
|
|
|
if(profitDist >= atr * BE_ATR_Multi)
|
|
|
|
|
|
{
|
|
|
|
|
|
double lock = atr * 0.2;
|
|
|
|
|
|
if(type==POSITION_TYPE_BUY)
|
|
|
|
|
|
{
|
|
|
|
|
|
double nSL = NormalizePrice(openPrice + lock);
|
|
|
|
|
|
if(sl < nSL)
|
|
|
|
|
|
{
|
|
|
|
|
|
trade.SetExpertMagicNumber(MagicSwing);
|
|
|
|
|
|
trade.PositionModify(ticket, nSL, tp);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
double nSL = NormalizePrice(openPrice - lock);
|
|
|
|
|
|
if(sl > nSL || sl == 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
trade.SetExpertMagicNumber(MagicSwing);
|
|
|
|
|
|
trade.PositionModify(ticket, nSL, tp);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Progressive Trailing + Dynamic TP Extension
|
|
|
|
|
|
if(profitDist >= atr * Trail_ATR_Multi)
|
|
|
|
|
|
{
|
|
|
|
|
|
double baseDist = atr * Trail_Step_ATR;
|
|
|
|
|
|
double profATR = profitDist / atr;
|
|
|
|
|
|
double tightF = MathMax(0.4, 1.0 - (profATR - Trail_ATR_Multi) * 0.15);
|
|
|
|
|
|
double trailDist = baseDist * tightF;
|
|
|
|
|
|
|
|
|
|
|
|
// Dynamic TP: jika harga sudah > 80% jarak ke TP, extend TP 1 ATR
|
|
|
|
|
|
double tpDist = (type==POSITION_TYPE_BUY) ? (tp - openPrice) : (openPrice - tp);
|
|
|
|
|
|
double newTP = tp;
|
|
|
|
|
|
if(tp > 0 && tpDist > 0 && profitDist >= tpDist * 0.8)
|
|
|
|
|
|
{
|
|
|
|
|
|
if(type==POSITION_TYPE_BUY)
|
|
|
|
|
|
newTP = NormalizePrice(tp + atr);
|
|
|
|
|
|
else
|
|
|
|
|
|
newTP = NormalizePrice(tp - atr);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if(type==POSITION_TYPE_BUY)
|
|
|
|
|
|
{
|
|
|
|
|
|
double nSL = NormalizePrice(price - trailDist);
|
2026-05-25 08:29:56 +07:00
|
|
|
|
// Trailing step minimal 10% dari ATR untuk mencegah spamming order modifikasi ke broker
|
|
|
|
|
|
if((nSL - sl >= atr * 0.1 || sl == 0) && nSL > openPrice)
|
2026-04-15 23:53:01 +07:00
|
|
|
|
{
|
|
|
|
|
|
trade.SetExpertMagicNumber(MagicSwing);
|
|
|
|
|
|
trade.PositionModify(ticket, nSL, newTP);
|
|
|
|
|
|
}
|
|
|
|
|
|
else if(newTP != tp && newTP > tp)
|
|
|
|
|
|
{
|
|
|
|
|
|
trade.SetExpertMagicNumber(MagicSwing);
|
|
|
|
|
|
trade.PositionModify(ticket, sl, newTP);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
double nSL = NormalizePrice(price + trailDist);
|
2026-05-25 08:29:56 +07:00
|
|
|
|
// Trailing step minimal 10% dari ATR
|
|
|
|
|
|
if((sl - nSL >= atr * 0.1 || sl == 0) && nSL < openPrice)
|
2026-04-15 23:53:01 +07:00
|
|
|
|
{
|
|
|
|
|
|
trade.SetExpertMagicNumber(MagicSwing);
|
|
|
|
|
|
trade.PositionModify(ticket, nSL, newTP);
|
|
|
|
|
|
}
|
|
|
|
|
|
else if(newTP != tp && newTP < tp)
|
|
|
|
|
|
{
|
|
|
|
|
|
trade.SetExpertMagicNumber(MagicSwing);
|
|
|
|
|
|
trade.PositionModify(ticket, sl, newTP);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
// MANAGE SCALP POSITIONS (fast BE + quick trail)
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
void ManageScalp()
|
|
|
|
|
|
{
|
|
|
|
|
|
double atrNow[];
|
|
|
|
|
|
ArraySetAsSeries(atrNow, true);
|
|
|
|
|
|
if(CopyBuffer(atr_handle, 0, 0, 1, atrNow) < 1) return;
|
|
|
|
|
|
double atr = atrNow[0];
|
|
|
|
|
|
if(atr <= 0) return;
|
|
|
|
|
|
|
|
|
|
|
|
// BB mid untuk dynamic TP
|
|
|
|
|
|
double bbMid[];
|
|
|
|
|
|
ArraySetAsSeries(bbMid, true);
|
|
|
|
|
|
if(CopyBuffer(bb_handle, 0, 0, 1, bbMid) < 1) return;
|
|
|
|
|
|
|
|
|
|
|
|
double beTrigger = atr * Scalp_BE_ATR; // Adaptive BE trigger
|
|
|
|
|
|
double lockDist = atr * 0.05; // Lock ~5% ATR profit
|
|
|
|
|
|
double trailDist = atr * Scalp_Trail_ATR; // Adaptive trail distance
|
|
|
|
|
|
double trailStart = beTrigger * 1.5; // Trail mulai setelah 1.5x BE trigger
|
|
|
|
|
|
|
|
|
|
|
|
for(int i = PositionsTotal()-1; i >= 0; i--)
|
|
|
|
|
|
{
|
|
|
|
|
|
ulong ticket = PositionGetTicket(i);
|
|
|
|
|
|
if(!PositionSelectByTicket(ticket)) continue;
|
|
|
|
|
|
if(PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
|
|
|
|
|
|
if(PositionGetInteger(POSITION_MAGIC) != MagicScalp) continue;
|
|
|
|
|
|
|
|
|
|
|
|
double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);
|
|
|
|
|
|
double sl = PositionGetDouble(POSITION_SL);
|
|
|
|
|
|
double tp = PositionGetDouble(POSITION_TP);
|
|
|
|
|
|
int type = (int)PositionGetInteger(POSITION_TYPE);
|
|
|
|
|
|
|
|
|
|
|
|
double price = (type==POSITION_TYPE_BUY) ?
|
|
|
|
|
|
SymbolInfoDouble(_Symbol,SYMBOL_BID) :
|
|
|
|
|
|
SymbolInfoDouble(_Symbol,SYMBOL_ASK);
|
|
|
|
|
|
|
|
|
|
|
|
double profitDist = (type==POSITION_TYPE_BUY) ?
|
|
|
|
|
|
(price-openPrice) : (openPrice-price);
|
|
|
|
|
|
|
|
|
|
|
|
// Adaptive Breakeven
|
|
|
|
|
|
if(profitDist >= beTrigger)
|
|
|
|
|
|
{
|
|
|
|
|
|
if(type==POSITION_TYPE_BUY)
|
|
|
|
|
|
{
|
|
|
|
|
|
double nSL = NormalizePrice(openPrice + lockDist);
|
|
|
|
|
|
if(sl < nSL)
|
|
|
|
|
|
{
|
|
|
|
|
|
trade.SetExpertMagicNumber(MagicScalp);
|
|
|
|
|
|
trade.PositionModify(ticket, nSL, tp);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
double nSL = NormalizePrice(openPrice - lockDist);
|
|
|
|
|
|
if(sl > nSL || sl == 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
trade.SetExpertMagicNumber(MagicScalp);
|
|
|
|
|
|
trade.PositionModify(ticket, nSL, tp);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Adaptive Trailing + Dynamic TP toward BB mid
|
|
|
|
|
|
if(profitDist >= trailStart)
|
|
|
|
|
|
{
|
2026-05-25 08:29:56 +07:00
|
|
|
|
// Dynamic TP: geser ke BB mid secara dinamis mengikuti pergerakan band
|
2026-04-15 23:53:01 +07:00
|
|
|
|
double newTP = tp;
|
2026-05-25 08:29:56 +07:00
|
|
|
|
if(type==POSITION_TYPE_BUY && bbMid[0] > price)
|
2026-04-15 23:53:01 +07:00
|
|
|
|
newTP = NormalizePrice(bbMid[0]);
|
2026-05-25 08:29:56 +07:00
|
|
|
|
else if(type==POSITION_TYPE_SELL && bbMid[0] < price)
|
2026-04-15 23:53:01 +07:00
|
|
|
|
newTP = NormalizePrice(bbMid[0]);
|
|
|
|
|
|
|
2026-05-25 08:29:56 +07:00
|
|
|
|
// Hanya modifikasi jika perbedaan TP cukup signifikan untuk mencegah spamming order ke broker
|
|
|
|
|
|
bool updateTP = (newTP != tp && MathAbs(tp - newTP) >= atr * 0.1);
|
|
|
|
|
|
|
2026-04-15 23:53:01 +07:00
|
|
|
|
if(type==POSITION_TYPE_BUY)
|
|
|
|
|
|
{
|
|
|
|
|
|
double nSL = NormalizePrice(price - trailDist);
|
2026-05-25 08:29:56 +07:00
|
|
|
|
// Trailing step minimal 5% dari ATR untuk scalp
|
|
|
|
|
|
if((nSL - sl >= atr * 0.05 || sl == 0) && nSL > openPrice)
|
2026-04-15 23:53:01 +07:00
|
|
|
|
{
|
|
|
|
|
|
trade.SetExpertMagicNumber(MagicScalp);
|
|
|
|
|
|
trade.PositionModify(ticket, nSL, newTP);
|
|
|
|
|
|
}
|
2026-05-25 08:29:56 +07:00
|
|
|
|
else if(updateTP)
|
2026-04-15 23:53:01 +07:00
|
|
|
|
{
|
|
|
|
|
|
trade.SetExpertMagicNumber(MagicScalp);
|
|
|
|
|
|
trade.PositionModify(ticket, sl, newTP);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
double nSL = NormalizePrice(price + trailDist);
|
2026-05-25 08:29:56 +07:00
|
|
|
|
// Trailing step minimal 5% dari ATR untuk scalp
|
|
|
|
|
|
if((sl - nSL >= atr * 0.05 || sl == 0) && nSL < openPrice)
|
2026-04-15 23:53:01 +07:00
|
|
|
|
{
|
|
|
|
|
|
trade.SetExpertMagicNumber(MagicScalp);
|
|
|
|
|
|
trade.PositionModify(ticket, nSL, newTP);
|
|
|
|
|
|
}
|
2026-05-25 08:29:56 +07:00
|
|
|
|
else if(updateTP)
|
2026-04-15 23:53:01 +07:00
|
|
|
|
{
|
|
|
|
|
|
trade.SetExpertMagicNumber(MagicScalp);
|
|
|
|
|
|
trade.PositionModify(ticket, sl, newTP);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
// ON TRADE EVENT
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
void OnTrade()
|
|
|
|
|
|
{
|
|
|
|
|
|
datetime dayStart = iTime(_Symbol, PERIOD_D1, 0);
|
|
|
|
|
|
|
|
|
|
|
|
// Reset counter saat ganti hari (fix day-boundary bug)
|
|
|
|
|
|
if(dayStart != lastTradeDay)
|
|
|
|
|
|
{
|
|
|
|
|
|
lastDealsTotal = 0;
|
|
|
|
|
|
lastTradeDay = dayStart;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
HistorySelect(dayStart, TimeCurrent());
|
|
|
|
|
|
int currentTotal = HistoryDealsTotal();
|
|
|
|
|
|
|
|
|
|
|
|
if(currentTotal > lastDealsTotal)
|
|
|
|
|
|
{
|
|
|
|
|
|
for(int i = lastDealsTotal; i < currentTotal; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
ulong ticket = HistoryDealGetTicket(i);
|
|
|
|
|
|
if(ticket == 0) continue;
|
|
|
|
|
|
long magic = (long)HistoryDealGetInteger(ticket, DEAL_MAGIC);
|
|
|
|
|
|
if(magic != MagicSwing && magic != MagicScalp) continue;
|
|
|
|
|
|
if(HistoryDealGetString(ticket, DEAL_SYMBOL) != _Symbol) continue;
|
|
|
|
|
|
if(HistoryDealGetInteger(ticket, DEAL_ENTRY) != DEAL_ENTRY_OUT) continue;
|
|
|
|
|
|
|
|
|
|
|
|
double profit = HistoryDealGetDouble(ticket, DEAL_PROFIT)
|
|
|
|
|
|
+ HistoryDealGetDouble(ticket, DEAL_SWAP)
|
|
|
|
|
|
+ HistoryDealGetDouble(ticket, DEAL_COMMISSION);
|
|
|
|
|
|
|
|
|
|
|
|
string mode = (magic == MagicScalp) ? "SCALP" : "SWING";
|
|
|
|
|
|
|
|
|
|
|
|
if(profit < 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
consecLosses++;
|
|
|
|
|
|
lastLossTime = (datetime)HistoryDealGetInteger(ticket, DEAL_TIME);
|
|
|
|
|
|
Print(mode, " LOSS #", consecLosses, " net=", DoubleToString(profit,2));
|
|
|
|
|
|
}
|
|
|
|
|
|
else if(profit > 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
if(consecLosses > 0)
|
|
|
|
|
|
Print(mode, " WIN +", DoubleToString(profit,2), " after ", consecLosses, " losses");
|
|
|
|
|
|
consecLosses = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Cleanup GlobalVariable partial close
|
|
|
|
|
|
ulong posId = (ulong)HistoryDealGetInteger(ticket, DEAL_POSITION_ID);
|
|
|
|
|
|
string gvP = "HUP5_SwP_" + IntegerToString(posId);
|
|
|
|
|
|
if(GlobalVariableCheck(gvP)) GlobalVariableDel(gvP);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
lastDealsTotal = currentTotal;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
// MAIN TICK
|
|
|
|
|
|
//============================================================
|
|
|
|
|
|
void OnTick()
|
|
|
|
|
|
{
|
|
|
|
|
|
// ===== Management setiap tick =====
|
|
|
|
|
|
ManageSwing();
|
|
|
|
|
|
ManageScalp();
|
|
|
|
|
|
|
|
|
|
|
|
// ===== Circuit breakers =====
|
|
|
|
|
|
if(!EquitySafe()) return;
|
|
|
|
|
|
CheckWeekendClose();
|
|
|
|
|
|
|
|
|
|
|
|
// ===== Bar baru check =====
|
|
|
|
|
|
datetime now = iTime(_Symbol, _Period, 0);
|
|
|
|
|
|
bool newBar = (now != lastSwingBar);
|
|
|
|
|
|
if(newBar) lastSwingBar = now;
|
|
|
|
|
|
|
|
|
|
|
|
if(!newBar) return; // Semua entry hanya di bar baru
|
|
|
|
|
|
|
|
|
|
|
|
// ===== Global filters =====
|
|
|
|
|
|
if(!DailySafe()) return;
|
|
|
|
|
|
if(!CooldownOK()) return;
|
|
|
|
|
|
if(DailyTargetReached()) return; // Sudah cukup profit hari ini
|
|
|
|
|
|
if(!MarginOK()) return; // Margin terlalu rendah
|
|
|
|
|
|
|
|
|
|
|
|
// ===== Load indicators =====
|
|
|
|
|
|
if(!LoadIndicators()) return;
|
|
|
|
|
|
|
|
|
|
|
|
double atr = atr_val[1];
|
2026-05-25 08:29:56 +07:00
|
|
|
|
if(atr < _Point * 10) return; // Proteksi batas spread dasar (sangat kecil)
|
|
|
|
|
|
if(!IsVolatilityOk()) return; // Filter volatilitas adaptif
|
2026-04-15 23:53:01 +07:00
|
|
|
|
|
|
|
|
|
|
double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
|
|
|
|
|
|
double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
|
|
|
|
|
|
|
|
|
|
|
|
int totalPos = CountAllMyPositions();
|
|
|
|
|
|
|
|
|
|
|
|
// =========================================================
|
|
|
|
|
|
// ENGINE 1: SWING ENTRIES
|
|
|
|
|
|
// =========================================================
|
|
|
|
|
|
if(SpreadOK(MaxSpread) && SessionOK() && !IsNearNewsHour() && totalPos < MaxTotalPos)
|
|
|
|
|
|
{
|
|
|
|
|
|
int swingPos = CountPositions(MagicSwing);
|
|
|
|
|
|
|
|
|
|
|
|
// --- SWING BUY ---
|
|
|
|
|
|
if(SwingBuySignal() && swingPos < MaxSwingPos)
|
|
|
|
|
|
{
|
|
|
|
|
|
if(CloseOnOpposite) ClosePositions(MagicSwing, +1); // close sells
|
|
|
|
|
|
|
|
|
|
|
|
double slP = NormalizePrice(iLow(_Symbol,_Period,1) - atr * ATR_SL_Multi);
|
|
|
|
|
|
if(slP < ask)
|
|
|
|
|
|
{
|
|
|
|
|
|
double minD = atr * 0.5;
|
|
|
|
|
|
if((ask - slP) < minD) slP = NormalizePrice(ask - minD);
|
|
|
|
|
|
|
|
|
|
|
|
double tpP = NormalizePrice(ask + (ask - slP) * Swing_RR);
|
|
|
|
|
|
|
|
|
|
|
|
if(ValidateSLTP(ask, slP, tpP, +1))
|
|
|
|
|
|
{
|
|
|
|
|
|
double lot = LotCalc((ask - slP) / _Point, RiskPercent);
|
|
|
|
|
|
if(lot > 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
trade.SetExpertMagicNumber(MagicSwing);
|
|
|
|
|
|
if(trade.Buy(lot, _Symbol, ask, slP, tpP, "SWING BUY"))
|
|
|
|
|
|
Print("SWING BUY: Lot=", lot, " SL=", slP, " TP=", tpP);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- SWING SELL ---
|
|
|
|
|
|
if(SwingSellSignal() && swingPos < MaxSwingPos)
|
|
|
|
|
|
{
|
|
|
|
|
|
if(CloseOnOpposite) ClosePositions(MagicSwing, -1); // close buys
|
|
|
|
|
|
|
|
|
|
|
|
double slP = NormalizePrice(iHigh(_Symbol,_Period,1) + atr * ATR_SL_Multi);
|
|
|
|
|
|
if(slP > bid)
|
|
|
|
|
|
{
|
|
|
|
|
|
double minD = atr * 0.5;
|
|
|
|
|
|
if((slP - bid) < minD) slP = NormalizePrice(bid + minD);
|
|
|
|
|
|
|
|
|
|
|
|
double tpP = NormalizePrice(bid - (slP - bid) * Swing_RR);
|
|
|
|
|
|
|
|
|
|
|
|
if(ValidateSLTP(bid, slP, tpP, -1))
|
|
|
|
|
|
{
|
|
|
|
|
|
double lot = LotCalc((slP - bid) / _Point, RiskPercent);
|
|
|
|
|
|
if(lot > 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
trade.SetExpertMagicNumber(MagicSwing);
|
|
|
|
|
|
if(trade.Sell(lot, _Symbol, bid, slP, tpP, "SWING SELL"))
|
|
|
|
|
|
Print("SWING SELL: Lot=", lot, " SL=", slP, " TP=", tpP);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
totalPos = CountAllMyPositions(); // Refresh setelah swing mungkin buka posisi
|
|
|
|
|
|
|
|
|
|
|
|
// =========================================================
|
|
|
|
|
|
// ENGINE 2: SCALP ENTRIES
|
|
|
|
|
|
// =========================================================
|
|
|
|
|
|
if(EnableScalp && SpreadOK(Scalp_MaxSpread) && ScalpSessionOK() && !IsNearNewsHour() && totalPos < MaxTotalPos)
|
|
|
|
|
|
{
|
|
|
|
|
|
int scalpPos = CountPositions(MagicScalp);
|
|
|
|
|
|
|
|
|
|
|
|
// --- SCALP BUY (BB lower bounce) ---
|
|
|
|
|
|
if(ScalpBuySignal() && scalpPos < MaxScalpPos)
|
|
|
|
|
|
{
|
|
|
|
|
|
double slDist = atr * Scalp_SL_ATR;
|
|
|
|
|
|
double slP = NormalizePrice(ask - slDist);
|
|
|
|
|
|
double tpP = NormalizePrice(ask + slDist * Scalp_RR);
|
|
|
|
|
|
|
|
|
|
|
|
// TP tidak melewati BB mid (target realistis)
|
|
|
|
|
|
if(tpP > bb_mid[0])
|
|
|
|
|
|
tpP = NormalizePrice(bb_mid[0]);
|
|
|
|
|
|
|
|
|
|
|
|
// Pastikan masih ada ruang profit minimal 0.5x SL
|
|
|
|
|
|
if((tpP - ask) < slDist * 0.5) { /* skip, target terlalu kecil */ }
|
|
|
|
|
|
else if(ValidateSLTP(ask, slP, tpP, +1))
|
|
|
|
|
|
{
|
|
|
|
|
|
double lot = LotCalc(slDist / _Point, ScalpRiskPercent);
|
|
|
|
|
|
if(lot > 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
trade.SetExpertMagicNumber(MagicScalp);
|
|
|
|
|
|
if(trade.Buy(lot, _Symbol, ask, slP, tpP, "SCALP BUY"))
|
|
|
|
|
|
Print("SCALP BUY: Lot=", lot, " SL=", slP, " TP=", tpP,
|
|
|
|
|
|
" BB_low=", DoubleToString(bb_lower[1],_Digits));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// --- SCALP SELL (BB upper bounce) ---
|
|
|
|
|
|
if(ScalpSellSignal() && scalpPos < MaxScalpPos)
|
|
|
|
|
|
{
|
|
|
|
|
|
double slDist = atr * Scalp_SL_ATR;
|
|
|
|
|
|
double slP = NormalizePrice(bid + slDist);
|
|
|
|
|
|
double tpP = NormalizePrice(bid - slDist * Scalp_RR);
|
|
|
|
|
|
|
|
|
|
|
|
// TP tidak melewati BB mid
|
|
|
|
|
|
if(tpP < bb_mid[0])
|
|
|
|
|
|
tpP = NormalizePrice(bb_mid[0]);
|
|
|
|
|
|
|
|
|
|
|
|
// Pastikan masih ada ruang profit minimal 0.5x SL
|
|
|
|
|
|
if((bid - tpP) < slDist * 0.5) { /* skip */ }
|
|
|
|
|
|
else if(ValidateSLTP(bid, slP, tpP, -1))
|
|
|
|
|
|
{
|
|
|
|
|
|
double lot = LotCalc(slDist / _Point, ScalpRiskPercent);
|
|
|
|
|
|
if(lot > 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
trade.SetExpertMagicNumber(MagicScalp);
|
|
|
|
|
|
if(trade.Sell(lot, _Symbol, bid, slP, tpP, "SCALP SELL"))
|
|
|
|
|
|
Print("SCALP SELL: Lot=", lot, " SL=", slP, " TP=", tpP,
|
|
|
|
|
|
" BB_up=", DoubleToString(bb_upper[1],_Digits));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|