1245 lines
44 KiB
Plaintext
1245 lines
44 KiB
Plaintext
//+------------------------------------------------------------------+
|
||
//| 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
|
||
int lastDealsTotal = 0; // Track total deal terakhir untuk OnTrade
|
||
|
||
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();
|
||
|
||
// 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();
|
||
|
||
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;
|
||
// Scan history 30 hari ke belakang untuk consec losses lintas hari
|
||
HistorySelect(TimeCurrent() - 30 * 24 * 3600, TimeCurrent());
|
||
|
||
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;
|
||
if(CopyBuffer(atr_handle, 0, 0, 50, atr_val) < 50) return false;
|
||
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;
|
||
}
|
||
|
||
//============================================================
|
||
// 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);
|
||
}
|
||
|
||
//============================================================
|
||
// 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)
|
||
{
|
||
if((sl > 0 && (price - sl) < minDist) || (tp > 0 && (tp - price) < minDist)) return false;
|
||
}
|
||
else
|
||
{
|
||
if((sl > 0 && (sl - price) < minDist) || (tp > 0 && (price - tp) < minDist)) return false;
|
||
}
|
||
}
|
||
|
||
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);
|
||
if(direction == +1 && sl > 0 && (ask - sl) < freezeDist) return false;
|
||
if(direction == -1 && sl > 0 && (sl - bid) < freezeDist) return false;
|
||
}
|
||
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; }
|
||
|
||
//============================================================
|
||
// 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;
|
||
}
|
||
|
||
//============================================================
|
||
// 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;
|
||
|
||
// 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;
|
||
|
||
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);
|
||
bool pinbar = IsPinBar(1, +1);
|
||
return (breakout || engulfing || pinbar);
|
||
}
|
||
|
||
//============================================================
|
||
// 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;
|
||
|
||
// 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;
|
||
|
||
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);
|
||
bool pinbar = IsPinBar(1, -1);
|
||
return (breakout || engulfing || pinbar);
|
||
}
|
||
|
||
//============================================================
|
||
// 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);
|
||
// Trailing step minimal 10% dari ATR untuk mencegah spamming order modifikasi ke broker
|
||
if((nSL - sl >= atr * 0.1 || sl == 0) && nSL > openPrice)
|
||
{
|
||
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);
|
||
// Trailing step minimal 10% dari ATR
|
||
if((sl - nSL >= atr * 0.1 || sl == 0) && nSL < openPrice)
|
||
{
|
||
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)
|
||
{
|
||
// Dynamic TP: geser ke BB mid secara dinamis mengikuti pergerakan band
|
||
double newTP = tp;
|
||
if(type==POSITION_TYPE_BUY && bbMid[0] > price)
|
||
newTP = NormalizePrice(bbMid[0]);
|
||
else if(type==POSITION_TYPE_SELL && bbMid[0] < price)
|
||
newTP = NormalizePrice(bbMid[0]);
|
||
|
||
// Hanya modifikasi jika perbedaan TP cukup signifikan untuk mencegah spamming order ke broker
|
||
bool updateTP = (newTP != tp && MathAbs(tp - newTP) >= atr * 0.1);
|
||
|
||
if(type==POSITION_TYPE_BUY)
|
||
{
|
||
double nSL = NormalizePrice(price - trailDist);
|
||
// Trailing step minimal 5% dari ATR untuk scalp
|
||
if((nSL - sl >= atr * 0.05 || sl == 0) && nSL > openPrice)
|
||
{
|
||
trade.SetExpertMagicNumber(MagicScalp);
|
||
trade.PositionModify(ticket, nSL, newTP);
|
||
}
|
||
else if(updateTP)
|
||
{
|
||
trade.SetExpertMagicNumber(MagicScalp);
|
||
trade.PositionModify(ticket, sl, newTP);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
double nSL = NormalizePrice(price + trailDist);
|
||
// Trailing step minimal 5% dari ATR untuk scalp
|
||
if((sl - nSL >= atr * 0.05 || sl == 0) && nSL < openPrice)
|
||
{
|
||
trade.SetExpertMagicNumber(MagicScalp);
|
||
trade.PositionModify(ticket, nSL, newTP);
|
||
}
|
||
else if(updateTP)
|
||
{
|
||
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];
|
||
if(atr < _Point * 10) return; // Proteksi batas spread dasar (sangat kecil)
|
||
if(!IsVolatilityOk()) return; // Filter volatilitas adaptif
|
||
|
||
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));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} |