826 lines
30 KiB
Plaintext
826 lines
30 KiB
Plaintext
//+------------------------------------------------------------------+
|
|
//| ConservativeScalper.mq4 |
|
|
//| MT4 Conservative Scalping Expert Advisor|
|
|
//| |
|
|
//| Strategy: |
|
|
//| - M15 trend bias via EMA 50/200 |
|
|
//| - M5 execution via EMA 20 + RSI 14 + candle breakout |
|
|
//| - Session, spread, ATR, rollover, and day-of-week filters |
|
|
//| - Fixed-fractional risk sizing (default 0.25% per trade) |
|
|
//| - Hard SL/TP on every trade — no martingale, no grid |
|
|
//| - Break-even, trailing stop, time-based exit |
|
|
//| - Daily loss cap, max trades/day, consecutive loss pause |
|
|
//| - Equity drawdown hard stop |
|
|
//| |
|
|
//| Pairs: EURUSD, GBPUSD, USDJPY (tune per pair) |
|
|
//| Timeframe: M5 (with M15 bias) |
|
|
//+------------------------------------------------------------------+
|
|
#property copyright "NAK"
|
|
#property link "https://github.com/NadirAliOffical/conservative-scalper-ea"
|
|
#property version "1.00"
|
|
#property strict
|
|
|
|
//====================================================================
|
|
// GENERAL INPUTS
|
|
//====================================================================
|
|
extern int MagicNumber = 20260409; // Unique EA identifier
|
|
extern string TradeComment = "CScalp"; // Order comment tag
|
|
extern bool EnableLong = true; // Allow buy trades
|
|
extern bool EnableShort = true; // Allow sell trades
|
|
extern bool OneTradePerSymbol = true; // One open trade per symbol
|
|
extern bool AllowNewTrades = true; // Master on/off switch
|
|
|
|
//====================================================================
|
|
// SESSION / TIME FILTERS
|
|
//====================================================================
|
|
extern int SessionStartHour = 8; // Server hour to start trading
|
|
extern int SessionEndHour = 17; // Server hour to stop new trades
|
|
extern bool AllowMonday = true;
|
|
extern bool AllowTuesday = true;
|
|
extern bool AllowWednesday = true;
|
|
extern bool AllowThursday = true;
|
|
extern bool AllowFriday = false; // Off by default — thin close
|
|
extern int RolloverBlockBefore = 30; // Mins to block before 00:00
|
|
extern int RolloverBlockAfter = 30; // Mins to block after 00:00
|
|
|
|
//====================================================================
|
|
// BIAS INDICATORS (Higher timeframe)
|
|
//====================================================================
|
|
extern ENUM_TIMEFRAMES BiasTimeframe = PERIOD_M15; // Trend filter timeframe
|
|
extern int BiasFastEMA = 50; // Fast EMA period on bias TF
|
|
extern int BiasSlowEMA = 200; // Slow EMA period on bias TF
|
|
|
|
//====================================================================
|
|
// EXECUTION INDICATORS (Chart timeframe — run EA on M5)
|
|
//====================================================================
|
|
extern int ExecEMA_Period = 20; // EMA for local direction
|
|
extern int RSI_Period = 14; // RSI period
|
|
extern double RSI_LongLevel = 50.0; // RSI cross-above for longs
|
|
extern double RSI_ShortLevel = 50.0; // RSI cross-below for shorts
|
|
extern int ATR_Period = 14; // ATR period
|
|
extern double MinATR_Pips = 3.0; // Min ATR (pips) — avoid dead mkt
|
|
extern int BreakoutBars = 1; // Bars back for high/low breakout
|
|
|
|
//====================================================================
|
|
// RISK SIZING
|
|
//====================================================================
|
|
extern int LotSizingMode = 1; // 0=Fixed lot 1=Risk %
|
|
extern double FixedLot = 0.01; // Used when mode=0
|
|
extern double RiskPercent = 0.50; // % of equity risked per trade
|
|
extern double MaxSpreadPips = 2.5; // Max allowed spread in pips
|
|
extern int MaxSlippagePts = 3; // Max slippage in broker points
|
|
|
|
//====================================================================
|
|
// STOP LOSS / TAKE PROFIT
|
|
//====================================================================
|
|
extern int StopLossMode = 1; // 0=Fixed pips 1=ATR multiple
|
|
extern double StopLossPips = 8.0; // Fixed SL (pips) mode=0
|
|
extern double StopLossATRMult = 1.2; // ATR multiplier for SL mode=1
|
|
extern int TakeProfitMode = 1; // 0=Fixed pips 1=ATR multiple
|
|
extern double TakeProfitPips = 10.0; // Fixed TP (pips) mode=0
|
|
extern double TakeProfitATRMult = 1.2; // ATR multiplier for TP mode=1
|
|
|
|
//====================================================================
|
|
// TRADE MANAGEMENT
|
|
//====================================================================
|
|
extern bool UseBreakEven = true;
|
|
extern double BreakEvenTriggerR = 0.8; // Move SL to BE after 0.8R profit
|
|
extern double BreakEvenOffsetPips = 0.5; // Buffer pips beyond entry for BE
|
|
extern bool UseTrailingStop = false;
|
|
extern double TrailingStartR = 1.0; // Start trailing after 1R profit
|
|
extern double TrailingDistancePips = 5.0; // Trail distance in pips
|
|
extern bool UseTimeExit = true;
|
|
extern int MaxTradeMinutes = 20; // Close stalled trades after N min
|
|
extern bool CloseAtSessionEnd = true; // Close open trades at session end
|
|
|
|
//====================================================================
|
|
// DAILY / SESSION PROTECTION
|
|
//====================================================================
|
|
extern int MaxTradesPerDay = 6; // Max new trades per session day
|
|
extern double MaxDailyLossPercent = 2.0; // Stop trading if daily loss >= X%
|
|
extern int MaxConsecutiveLosses = 3; // Pause after N consecutive losses
|
|
extern double MaxDrawdownPercent = 20.0; // Hard stop if equity DD >= X%
|
|
extern double MaxTotalOpenRiskPct = 1.0; // Cap on total open risk %
|
|
|
|
//====================================================================
|
|
// NEWS FILTER (auto-fetches ForexFactory calendar)
|
|
//====================================================================
|
|
extern bool UseNewsFilter = true; // Enable automatic news filter
|
|
extern string NewsFilterCurrencies = "USD,EUR,GBP";// Block news for these currencies
|
|
extern int NewsBlockMinsBefore = 30; // Mins to block before event
|
|
extern int NewsBlockMinsAfter = 30; // Mins to block after event
|
|
extern int BrokerGMTOffset = 2; // Broker server GMT offset (check chart)
|
|
|
|
//====================================================================
|
|
// GLOBALS
|
|
//====================================================================
|
|
double g_pip; // Value of 1 pip in price units
|
|
double g_point; // Broker point
|
|
int g_digits; // Symbol digits
|
|
int g_todayTrades; // Trades opened today
|
|
double g_todayStartEquity; // Equity at start of today
|
|
int g_consecutiveLosses; // Rolling loss streak count
|
|
int g_lastHistoryTotal; // History size snapshot (for tracking closed orders)
|
|
bool g_tradingHalted; // True when max DD hit (persists across days)
|
|
double g_peakEquity; // All-time equity high for DD calculation
|
|
datetime g_lastTradeDay; // Date of last counter reset
|
|
|
|
// News filter globals
|
|
datetime g_newsEvents[];
|
|
int g_newsEventCount = 0;
|
|
datetime g_lastNewsFetch = 0;
|
|
datetime g_lastNewsLogTime = 0;
|
|
|
|
// GlobalVariable key names (set in OnInit)
|
|
string g_gvPeak;
|
|
string g_gvHalt;
|
|
string g_gvConsec;
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| INIT |
|
|
//+------------------------------------------------------------------+
|
|
int OnInit()
|
|
{
|
|
g_digits = (int)MarketInfo(Symbol(), MODE_DIGITS);
|
|
|
|
// Normalise pip for 4-digit and 5-digit brokers
|
|
if(g_digits == 5 || g_digits == 3)
|
|
g_pip = Point * 10;
|
|
else
|
|
g_pip = Point;
|
|
g_point = Point;
|
|
|
|
// GlobalVariable keys unique to this symbol + magic number
|
|
string suffix = Symbol() + "_" + IntegerToString(MagicNumber);
|
|
g_gvPeak = "CScalp_Peak_" + suffix;
|
|
g_gvHalt = "CScalp_Halt_" + suffix;
|
|
g_gvConsec = "CScalp_Consec_" + suffix;
|
|
|
|
g_todayTrades = 0;
|
|
g_todayStartEquity = AccountEquity();
|
|
g_lastHistoryTotal = OrdersHistoryTotal();
|
|
g_lastTradeDay = 0;
|
|
|
|
// Restore persistent state so restarts don't reset DD protection
|
|
g_peakEquity = GlobalVariableCheck(g_gvPeak) ? GlobalVariableGet(g_gvPeak) : AccountEquity();
|
|
g_tradingHalted = GlobalVariableCheck(g_gvHalt) && GlobalVariableGet(g_gvHalt) > 0;
|
|
g_consecutiveLosses = GlobalVariableCheck(g_gvConsec) ? (int)GlobalVariableGet(g_gvConsec) : 0;
|
|
|
|
Log("Initialized | Symbol=" + Symbol() +
|
|
" Digits=" + IntegerToString(g_digits) +
|
|
" Pip=" + DoubleToString(g_pip, g_digits + 1));
|
|
|
|
return INIT_SUCCEEDED;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| DEINIT |
|
|
//+------------------------------------------------------------------+
|
|
void OnDeinit(const int reason)
|
|
{
|
|
Log("Deinitialized | Reason=" + IntegerToString(reason));
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| TICK |
|
|
//+------------------------------------------------------------------+
|
|
void OnTick()
|
|
{
|
|
// 1. Reset daily counters if calendar date changed
|
|
ResetDailyIfNewDay();
|
|
|
|
// 2a. Refresh news calendar once per day
|
|
FetchNewsCalendar();
|
|
|
|
// 2. Track peak equity
|
|
if(AccountEquity() > g_peakEquity)
|
|
{
|
|
g_peakEquity = AccountEquity();
|
|
GlobalVariableSet(g_gvPeak, g_peakEquity);
|
|
}
|
|
|
|
// 3. Track closed order outcomes (update consecutive loss counter)
|
|
TrackClosedOrders();
|
|
|
|
// 4. Manage existing open trades (BE, trail, time/session exit)
|
|
ManageOpenTrades();
|
|
|
|
// 5. Evaluate new entry
|
|
if(!CanOpenNewTrade()) return;
|
|
|
|
int signal = GetEntrySignal();
|
|
if(signal != 0) ExecuteTrade(signal);
|
|
}
|
|
|
|
//====================================================================
|
|
// DAILY RESET
|
|
//====================================================================
|
|
void ResetDailyIfNewDay()
|
|
{
|
|
datetime today = StringToTime(TimeToStr(TimeCurrent(), TIME_DATE));
|
|
if(today == g_lastTradeDay) return;
|
|
|
|
g_lastTradeDay = today;
|
|
g_todayTrades = 0;
|
|
g_todayStartEquity = AccountEquity();
|
|
|
|
// Consecutive loss streak is NOT reset on new day — only a win resets it
|
|
|
|
Log("New day reset | Equity=" + DoubleToString(AccountEquity(), 2));
|
|
}
|
|
|
|
//====================================================================
|
|
// PRE-TRADE GATE CHECKS
|
|
//====================================================================
|
|
bool CanOpenNewTrade()
|
|
{
|
|
if(!AllowNewTrades) return false;
|
|
if(g_tradingHalted) return false;
|
|
|
|
// Hard drawdown check
|
|
if(g_peakEquity > 0)
|
|
{
|
|
double dd = (g_peakEquity - AccountEquity()) / g_peakEquity * 100.0;
|
|
if(dd >= MaxDrawdownPercent)
|
|
{
|
|
Log("HARD HALT — max drawdown " + DoubleToString(dd, 2) + "% reached");
|
|
g_tradingHalted = true;
|
|
GlobalVariableSet(g_gvHalt, 1.0);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Daily loss cap
|
|
if(g_todayStartEquity > 0)
|
|
{
|
|
double dailyLoss = (g_todayStartEquity - AccountEquity()) / g_todayStartEquity * 100.0;
|
|
if(dailyLoss >= MaxDailyLossPercent) return false;
|
|
}
|
|
|
|
// Max trades today
|
|
if(g_todayTrades >= MaxTradesPerDay) return false;
|
|
|
|
// Consecutive loss pause
|
|
if(g_consecutiveLosses >= MaxConsecutiveLosses) return false;
|
|
|
|
// Day of week
|
|
if(!IsAllowedDay()) return false;
|
|
|
|
// Session hours
|
|
if(!IsSessionTime()) return false;
|
|
|
|
// Rollover block
|
|
if(IsRolloverTime()) return false;
|
|
|
|
// News filter
|
|
if(IsNewsTime()) return false;
|
|
|
|
// Spread
|
|
double spreadPips = MarketInfo(Symbol(), MODE_SPREAD) * g_point / g_pip;
|
|
if(spreadPips > MaxSpreadPips) return false;
|
|
|
|
// ATR minimum (avoid dead market)
|
|
double atrPips = iATR(Symbol(), Period(), ATR_Period, 1) / g_pip;
|
|
if(atrPips < MinATR_Pips) return false;
|
|
|
|
// One trade per symbol
|
|
if(OneTradePerSymbol && HasOpenTrade()) return false;
|
|
|
|
// Total open risk cap
|
|
if(GetTotalOpenRiskPct() >= MaxTotalOpenRiskPct) return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
//====================================================================
|
|
// ENTRY SIGNAL
|
|
// Returns: 1 = BUY -1 = SELL 0 = NONE
|
|
//====================================================================
|
|
int GetEntrySignal()
|
|
{
|
|
// --- Higher timeframe bias (M15 EMA 50 vs 200) ---
|
|
double biasFast = iMA(Symbol(), BiasTimeframe, BiasFastEMA, 0, MODE_EMA, PRICE_CLOSE, 1);
|
|
double biasSlow = iMA(Symbol(), BiasTimeframe, BiasSlowEMA, 0, MODE_EMA, PRICE_CLOSE, 1);
|
|
double biasFastPrev = iMA(Symbol(), BiasTimeframe, BiasFastEMA, 0, MODE_EMA, PRICE_CLOSE, 2);
|
|
|
|
bool bullBias = (biasFast > biasSlow) && (biasFast >= biasFastPrev);
|
|
bool bearBias = (biasFast < biasSlow) && (biasFast <= biasFastPrev);
|
|
|
|
// --- Execution timeframe indicators ---
|
|
double execEMA = iMA(Symbol(), Period(), ExecEMA_Period, 0, MODE_EMA, PRICE_CLOSE, 1);
|
|
double rsiNow = iRSI(Symbol(), Period(), RSI_Period, PRICE_CLOSE, 1);
|
|
double rsiPrev = iRSI(Symbol(), Period(), RSI_Period, PRICE_CLOSE, 2);
|
|
|
|
double prevHigh = iHigh(Symbol(), Period(), BreakoutBars + 1);
|
|
double prevLow = iLow(Symbol(), Period(), BreakoutBars + 1);
|
|
double closeNow = iClose(Symbol(), Period(), 1);
|
|
|
|
// --- LONG ---
|
|
if(EnableLong && bullBias)
|
|
{
|
|
bool aboveEMA = (closeNow > execEMA);
|
|
bool rsiCross = (rsiNow >= RSI_LongLevel) && (rsiPrev < RSI_LongLevel);
|
|
bool breakout = (closeNow > prevHigh);
|
|
|
|
if(aboveEMA && rsiCross && breakout) return 1;
|
|
}
|
|
|
|
// --- SHORT ---
|
|
if(EnableShort && bearBias)
|
|
{
|
|
bool belowEMA = (closeNow < execEMA);
|
|
bool rsiCross = (rsiNow <= RSI_ShortLevel) && (rsiPrev > RSI_ShortLevel);
|
|
bool breakout = (closeNow < prevLow);
|
|
|
|
if(belowEMA && rsiCross && breakout) return -1;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
//====================================================================
|
|
// EXECUTE TRADE
|
|
//====================================================================
|
|
void ExecuteTrade(int direction)
|
|
{
|
|
double atr = iATR(Symbol(), Period(), ATR_Period, 1);
|
|
|
|
// SL distance
|
|
double slDist = (StopLossMode == 0)
|
|
? StopLossPips * g_pip
|
|
: StopLossATRMult * atr;
|
|
|
|
// TP distance
|
|
double tpDist = (TakeProfitMode == 0)
|
|
? TakeProfitPips * g_pip
|
|
: TakeProfitATRMult * atr;
|
|
|
|
// Enforce broker minimum stop level
|
|
double minStop = MarketInfo(Symbol(), MODE_STOPLEVEL) * g_point;
|
|
if(slDist < minStop + g_pip) slDist = minStop + g_pip;
|
|
if(tpDist < minStop + g_pip) tpDist = minStop + g_pip;
|
|
|
|
// Lot size
|
|
double lots = (LotSizingMode == 0)
|
|
? FixedLot
|
|
: CalcLotByRisk(slDist);
|
|
lots = NormalizeLots(lots);
|
|
|
|
if(lots <= 0)
|
|
{
|
|
Log("ERROR: Lot size <=0 — trade skipped");
|
|
return;
|
|
}
|
|
|
|
// Free margin check
|
|
double reqMargin = MarketInfo(Symbol(), MODE_MARGINREQUIRED) * lots;
|
|
if(AccountFreeMargin() < reqMargin)
|
|
{
|
|
Log("ERROR: Insufficient margin — trade skipped");
|
|
return;
|
|
}
|
|
|
|
double sl, tp;
|
|
int cmd;
|
|
double price;
|
|
color arrowCol;
|
|
|
|
if(direction == 1)
|
|
{
|
|
cmd = OP_BUY;
|
|
price = Ask;
|
|
sl = NormalizeDouble(price - slDist, g_digits);
|
|
tp = NormalizeDouble(price + tpDist, g_digits);
|
|
arrowCol = clrDodgerBlue;
|
|
}
|
|
else
|
|
{
|
|
cmd = OP_SELL;
|
|
price = Bid;
|
|
sl = NormalizeDouble(price + slDist, g_digits);
|
|
tp = NormalizeDouble(price - tpDist, g_digits);
|
|
arrowCol = clrOrangeRed;
|
|
}
|
|
|
|
int ticket = OrderSend(Symbol(), cmd, lots, price, MaxSlippagePts,
|
|
sl, tp, TradeComment, MagicNumber, 0, arrowCol);
|
|
|
|
if(ticket < 0)
|
|
{
|
|
Log("ORDER FAILED | Error=" + IntegerToString(GetLastError()) +
|
|
" Dir=" + IntegerToString(direction));
|
|
}
|
|
else
|
|
{
|
|
g_todayTrades++;
|
|
Log("ORDER OPEN | Ticket=" + IntegerToString(ticket) +
|
|
" Dir=" + IntegerToString(direction) +
|
|
" Lots=" + DoubleToString(lots, 2) +
|
|
" Price=" + DoubleToString(price, g_digits) +
|
|
" SL=" + DoubleToString(sl, g_digits) +
|
|
" TP=" + DoubleToString(tp, g_digits) +
|
|
" SLpips=" + DoubleToString(slDist / g_pip, 1));
|
|
}
|
|
}
|
|
|
|
//====================================================================
|
|
// MANAGE OPEN TRADES (BE, trail, time/session exits)
|
|
//====================================================================
|
|
void ManageOpenTrades()
|
|
{
|
|
for(int i = OrdersTotal() - 1; i >= 0; i--)
|
|
{
|
|
if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
|
|
if(OrderSymbol() != Symbol()) continue;
|
|
if(OrderMagicNumber() != MagicNumber) continue;
|
|
if(OrderType() > OP_SELL) continue; // skip pending
|
|
|
|
int ticket = OrderTicket();
|
|
int type = OrderType();
|
|
double openPrice = OrderOpenPrice();
|
|
double curSL = OrderStopLoss();
|
|
double curTP = OrderTakeProfit();
|
|
double slDist = MathAbs(openPrice - curSL);
|
|
|
|
// --- Session-end close ---
|
|
if(CloseAtSessionEnd && !IsSessionTime())
|
|
{
|
|
CloseOrder(ticket, type, "SessionEnd");
|
|
continue;
|
|
}
|
|
|
|
// --- Time-based exit ---
|
|
if(UseTimeExit)
|
|
{
|
|
int minsOpen = (int)((TimeCurrent() - OrderOpenTime()) / 60);
|
|
if(minsOpen >= MaxTradeMinutes)
|
|
{
|
|
CloseOrder(ticket, type, "TimeExit");
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// Current P&L in price units
|
|
double profit = (type == OP_BUY)
|
|
? Bid - openPrice
|
|
: openPrice - Ask;
|
|
double profitR = (slDist > 0) ? profit / slDist : 0;
|
|
|
|
// --- Break-even ---
|
|
if(UseBreakEven && slDist > 0 && profitR >= BreakEvenTriggerR)
|
|
{
|
|
double beOffset = BreakEvenOffsetPips * g_pip;
|
|
if(type == OP_BUY)
|
|
{
|
|
double newSL = NormalizeDouble(openPrice + beOffset, g_digits);
|
|
if(newSL > curSL + g_point)
|
|
{
|
|
if(OrderModify(ticket, openPrice, newSL, curTP, 0, clrGold))
|
|
Log("BE set | Ticket=" + IntegerToString(ticket) +
|
|
" NewSL=" + DoubleToString(newSL, g_digits));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
double newSL = NormalizeDouble(openPrice - beOffset, g_digits);
|
|
if(curSL == 0 || newSL < curSL - g_point)
|
|
{
|
|
if(OrderModify(ticket, openPrice, newSL, curTP, 0, clrGold))
|
|
Log("BE set | Ticket=" + IntegerToString(ticket) +
|
|
" NewSL=" + DoubleToString(newSL, g_digits));
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- Trailing stop ---
|
|
if(UseTrailingStop && slDist > 0 && profitR >= TrailingStartR)
|
|
{
|
|
double trailDist = TrailingDistancePips * g_pip;
|
|
if(type == OP_BUY)
|
|
{
|
|
double newSL = NormalizeDouble(Bid - trailDist, g_digits);
|
|
if(newSL > curSL + g_point)
|
|
{
|
|
if(OrderModify(ticket, openPrice, newSL, curTP, 0, clrAqua))
|
|
Log("Trail updated | Ticket=" + IntegerToString(ticket) +
|
|
" NewSL=" + DoubleToString(newSL, g_digits));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
double newSL = NormalizeDouble(Ask + trailDist, g_digits);
|
|
if(curSL == 0 || newSL < curSL - g_point)
|
|
{
|
|
if(OrderModify(ticket, openPrice, newSL, curTP, 0, clrAqua))
|
|
Log("Trail updated | Ticket=" + IntegerToString(ticket) +
|
|
" NewSL=" + DoubleToString(newSL, g_digits));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//====================================================================
|
|
// TRACK CLOSED ORDERS (update consecutive loss counter)
|
|
//====================================================================
|
|
void TrackClosedOrders()
|
|
{
|
|
int histTotal = OrdersHistoryTotal();
|
|
if(histTotal <= g_lastHistoryTotal) return;
|
|
|
|
for(int i = g_lastHistoryTotal; i < histTotal; i++)
|
|
{
|
|
if(!OrderSelect(i, SELECT_BY_POS, MODE_HISTORY)) continue;
|
|
if(OrderSymbol() != Symbol()) continue;
|
|
if(OrderMagicNumber() != MagicNumber) continue;
|
|
if(OrderType() > OP_SELL) continue;
|
|
|
|
double netPnl = OrderProfit() + OrderSwap() + OrderCommission();
|
|
|
|
if(netPnl < 0)
|
|
{
|
|
g_consecutiveLosses++;
|
|
GlobalVariableSet(g_gvConsec, (double)g_consecutiveLosses);
|
|
Log("LOSS | Ticket=" + IntegerToString(OrderTicket()) +
|
|
" PnL=" + DoubleToString(netPnl, 2) +
|
|
" ConsecLosses=" + IntegerToString(g_consecutiveLosses));
|
|
}
|
|
else
|
|
{
|
|
if(g_consecutiveLosses > 0)
|
|
Log("WIN — loss streak reset from " + IntegerToString(g_consecutiveLosses));
|
|
g_consecutiveLosses = 0;
|
|
GlobalVariableSet(g_gvConsec, 0.0);
|
|
}
|
|
}
|
|
g_lastHistoryTotal = histTotal;
|
|
}
|
|
|
|
//====================================================================
|
|
// CLOSE ORDER HELPER
|
|
//====================================================================
|
|
void CloseOrder(int ticket, int type, string reason)
|
|
{
|
|
double price = (type == OP_BUY) ? Bid : Ask;
|
|
bool ok = OrderClose(ticket, OrderLots(), price, MaxSlippagePts, clrWhite);
|
|
|
|
if(ok)
|
|
Log("ORDER CLOSED | Ticket=" + IntegerToString(ticket) + " Reason=" + reason);
|
|
else
|
|
Log("CLOSE FAILED | Ticket=" + IntegerToString(ticket) +
|
|
" Error=" + IntegerToString(GetLastError()));
|
|
}
|
|
|
|
//====================================================================
|
|
// POSITION SIZING
|
|
//====================================================================
|
|
double CalcLotByRisk(double slDist)
|
|
{
|
|
double equity = AccountEquity();
|
|
double riskAmt = equity * RiskPercent / 100.0;
|
|
double tickVal = MarketInfo(Symbol(), MODE_TICKVALUE);
|
|
double tickSize = MarketInfo(Symbol(), MODE_TICKSIZE);
|
|
|
|
if(tickVal <= 0 || tickSize <= 0 || slDist <= 0) return FixedLot;
|
|
|
|
double slTicks = slDist / tickSize;
|
|
return riskAmt / (slTicks * tickVal);
|
|
}
|
|
|
|
double NormalizeLots(double lots)
|
|
{
|
|
double minLot = MarketInfo(Symbol(), MODE_MINLOT);
|
|
double maxLot = MarketInfo(Symbol(), MODE_MAXLOT);
|
|
double lotStep = MarketInfo(Symbol(), MODE_LOTSTEP);
|
|
|
|
if(lotStep > 0)
|
|
lots = MathFloor(lots / lotStep) * lotStep;
|
|
|
|
return NormalizeDouble(MathMax(minLot, MathMin(maxLot, lots)), 2);
|
|
}
|
|
|
|
//====================================================================
|
|
// UTILITY FUNCTIONS
|
|
//====================================================================
|
|
bool HasOpenTrade()
|
|
{
|
|
for(int i = 0; i < OrdersTotal(); i++)
|
|
{
|
|
if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
|
|
if(OrderSymbol() == Symbol() && OrderMagicNumber() == MagicNumber)
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
double GetTotalOpenRiskPct()
|
|
{
|
|
double totalRisk = 0;
|
|
double equity = AccountEquity();
|
|
if(equity <= 0) return 0;
|
|
|
|
for(int i = 0; i < OrdersTotal(); i++)
|
|
{
|
|
if(!OrderSelect(i, SELECT_BY_POS, MODE_TRADES)) continue;
|
|
if(OrderMagicNumber() != MagicNumber) continue;
|
|
if(OrderType() > OP_SELL) continue;
|
|
|
|
double sl = OrderStopLoss();
|
|
if(sl == 0) continue;
|
|
|
|
double slDist = MathAbs(OrderOpenPrice() - sl);
|
|
double tickVal = MarketInfo(OrderSymbol(), MODE_TICKVALUE);
|
|
double tickSize = MarketInfo(OrderSymbol(), MODE_TICKSIZE);
|
|
if(tickSize <= 0) continue;
|
|
|
|
totalRisk += (slDist / tickSize) * tickVal * OrderLots() / equity * 100.0;
|
|
}
|
|
return totalRisk;
|
|
}
|
|
|
|
bool IsSessionTime()
|
|
{
|
|
int h = TimeHour(TimeCurrent());
|
|
return (h >= SessionStartHour && h < SessionEndHour);
|
|
}
|
|
|
|
bool IsRolloverTime()
|
|
{
|
|
int h = TimeHour(TimeCurrent());
|
|
int m = TimeMinute(TimeCurrent());
|
|
int totalMin = h * 60 + m;
|
|
|
|
// Minutes until next midnight
|
|
int beforeMid = 1440 - totalMin;
|
|
// Minutes since last midnight
|
|
int afterMid = totalMin;
|
|
|
|
return (beforeMid <= RolloverBlockBefore || afterMid <= RolloverBlockAfter);
|
|
}
|
|
|
|
bool IsAllowedDay()
|
|
{
|
|
int dow = TimeDayOfWeek(TimeCurrent());
|
|
switch(dow)
|
|
{
|
|
case 1: return AllowMonday;
|
|
case 2: return AllowTuesday;
|
|
case 3: return AllowWednesday;
|
|
case 4: return AllowThursday;
|
|
case 5: return AllowFriday;
|
|
default: return false;
|
|
}
|
|
}
|
|
|
|
//====================================================================
|
|
// NEWS FILTER — auto-fetches ForexFactory high-impact calendar
|
|
// Requires: MT4 Tools → Options → Expert Advisors →
|
|
// Allow WebRequest for: https://nfs.faireconomy.media
|
|
//====================================================================
|
|
void FetchNewsCalendar()
|
|
{
|
|
if(!UseNewsFilter) return;
|
|
|
|
datetime today = StringToTime(TimeToStr(TimeCurrent(), TIME_DATE));
|
|
if(g_lastNewsFetch == today) return; // Already fetched today
|
|
|
|
string url = "https://nfs.faireconomy.media/ff_calendar_thisweek.json";
|
|
string headers = "User-Agent: Mozilla/5.0\r\n";
|
|
char post[];
|
|
char result[];
|
|
string resultHeaders;
|
|
|
|
ResetLastError();
|
|
int httpCode = WebRequest("GET", url, headers, 10000, post, result, resultHeaders);
|
|
|
|
if(httpCode != 200)
|
|
{
|
|
Log("NEWS: Fetch failed. HTTP=" + IntegerToString(httpCode) +
|
|
" Error=" + IntegerToString(GetLastError()) +
|
|
" — Check WebRequest whitelist in MT4 options");
|
|
return;
|
|
}
|
|
|
|
string json = CharArrayToString(result);
|
|
ParseNewsJSON(json);
|
|
g_lastNewsFetch = today;
|
|
Log("NEWS: Calendar updated. High-impact events found=" + IntegerToString(g_newsEventCount));
|
|
}
|
|
|
|
void ParseNewsJSON(string json)
|
|
{
|
|
g_newsEventCount = 0;
|
|
ArrayResize(g_newsEvents, 200);
|
|
|
|
int pos = 0;
|
|
int jsonLen = StringLen(json);
|
|
|
|
while(pos < jsonLen)
|
|
{
|
|
// Find next JSON object
|
|
int objStart = StringFind(json, "{", pos);
|
|
if(objStart < 0) break;
|
|
int objEnd = StringFind(json, "}", objStart);
|
|
if(objEnd < 0) break;
|
|
|
|
string obj = StringSubstr(json, objStart, objEnd - objStart + 1);
|
|
|
|
// Only process High impact events
|
|
if(StringFind(obj, "\"impact\":\"High\"") >= 0)
|
|
{
|
|
// Check currency filter
|
|
string country = ExtractJSONString(obj, "country");
|
|
if(StringFind(NewsFilterCurrencies, country) >= 0)
|
|
{
|
|
// Parse date
|
|
string dateStr = ExtractJSONString(obj, "date");
|
|
datetime eventTime = ParseISODate(dateStr);
|
|
if(eventTime > 0 && g_newsEventCount < 200)
|
|
{
|
|
g_newsEvents[g_newsEventCount] = eventTime;
|
|
g_newsEventCount++;
|
|
string title = ExtractJSONString(obj, "title");
|
|
Log("NEWS: Loaded | " + country + " " + title +
|
|
" @ " + TimeToStr(eventTime, TIME_DATE | TIME_MINUTES));
|
|
}
|
|
}
|
|
}
|
|
pos = objEnd + 1;
|
|
}
|
|
ArrayResize(g_newsEvents, g_newsEventCount);
|
|
}
|
|
|
|
string ExtractJSONString(string obj, string key)
|
|
{
|
|
string search = "\"" + key + "\":\"";
|
|
int start = StringFind(obj, search);
|
|
if(start < 0) return "";
|
|
start += StringLen(search);
|
|
int end = StringFind(obj, "\"", start);
|
|
if(end < 0) return "";
|
|
return StringSubstr(obj, start, end - start);
|
|
}
|
|
|
|
datetime ParseISODate(string iso)
|
|
{
|
|
// Format: "2026-04-04T08:30:00-0400"
|
|
if(StringLen(iso) < 19) return 0;
|
|
|
|
int year = (int)StringToInteger(StringSubstr(iso, 0, 4));
|
|
int month = (int)StringToInteger(StringSubstr(iso, 5, 2));
|
|
int day = (int)StringToInteger(StringSubstr(iso, 8, 2));
|
|
int hour = (int)StringToInteger(StringSubstr(iso, 11, 2));
|
|
int min = (int)StringToInteger(StringSubstr(iso, 14, 2));
|
|
|
|
// Parse timezone offset (e.g. -0400 or +0000)
|
|
int tzOffsetSecs = 0;
|
|
int tzPos = StringFind(iso, "+", 19);
|
|
int tzSign = 1;
|
|
if(tzPos < 0) { tzPos = StringFind(iso, "-", 19); tzSign = -1; }
|
|
if(tzPos >= 0)
|
|
{
|
|
int tzH = (int)StringToInteger(StringSubstr(iso, tzPos + 1, 2));
|
|
int tzM = (int)StringToInteger(StringSubstr(iso, tzPos + 3, 2));
|
|
tzOffsetSecs = tzSign * (tzH * 3600 + tzM * 60);
|
|
}
|
|
|
|
// Build UTC datetime
|
|
string dtStr = StringFormat("%04d.%02d.%02d %02d:%02d", year, month, day, hour, min);
|
|
datetime utc = StringToTime(dtStr) - tzOffsetSecs;
|
|
|
|
// Convert UTC → broker server time
|
|
datetime serverTime = utc + BrokerGMTOffset * 3600;
|
|
return serverTime;
|
|
}
|
|
|
|
bool IsNewsTime()
|
|
{
|
|
if(!UseNewsFilter || g_newsEventCount == 0) return false;
|
|
|
|
datetime now = TimeCurrent();
|
|
int blockBefore = NewsBlockMinsBefore * 60;
|
|
int blockAfter = NewsBlockMinsAfter * 60;
|
|
|
|
for(int i = 0; i < g_newsEventCount; i++)
|
|
{
|
|
if(now >= g_newsEvents[i] - blockBefore &&
|
|
now <= g_newsEvents[i] + blockAfter)
|
|
{
|
|
if(g_newsEvents[i] != g_lastNewsLogTime)
|
|
{
|
|
Log("NEWS: Trading blocked near event @ " +
|
|
TimeToStr(g_newsEvents[i], TIME_DATE | TIME_MINUTES));
|
|
g_lastNewsLogTime = g_newsEvents[i];
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void Log(string msg)
|
|
{
|
|
Print("[CScalp] " + TimeToStr(TimeCurrent(), TIME_DATE | TIME_MINUTES) + " | " + msg);
|
|
}
|
|
//+------------------------------------------------------------------+
|