diff --git a/Experts/RsiMomentumEA.mq5 b/Experts/RsiMomentumEA.mq5 index 66353fe..6ea6169 100644 --- a/Experts/RsiMomentumEA.mq5 +++ b/Experts/RsiMomentumEA.mq5 @@ -4,7 +4,9 @@ //| RSI + EMA9/WMA45 trên RSI + EMA200(close), signal, mũi tên, panel | //+------------------------------------------------------------------+ #property copyright "RsiMomentumEA" -#property version "3.01" +#property version "3.13" + +#include //--- Input (khớp Indicators/RsiMomentumIndicator/Lib/Inputs.mqh) input group "Chỉ báo" @@ -31,6 +33,8 @@ input int InpArrowSize = 1; input group "Panel thông tin" input bool InpShowPanel = true; +input color InpPanelColorEMA9 = clrGold; // chữ giá trị EMA9 (vàng) +input color InpPanelColorWMA45 = clrDodgerBlue; // chữ giá trị WMA45 (xanh dương) input group "Cảnh báo / Notification (khi có entry mới)" input bool InpAlertPush = true; @@ -41,6 +45,20 @@ input string InpSoundSell = "alert2.wav"; input bool InpAlertEmail = false; input bool InpAlertOnBar0 = false; +input group "Giao dịch tự động" +input bool InpTradeEnabled = true; +input ulong InpMagic = 202602; +input double InpRiskPercent = 1.0; // % balance mất nếu SL khớp (theo lot tính từ SL) +input double InpRewardRiskRatio = 1.1; // R:R — khoảng TP = tỷ lệ này × khoảng SL (ví dụ 1.5 = 1:1.5) +input int InpSwingMaxBars = 30; // quét swing pivot / fallback min-max +input int InpSlippagePoints = 30; +input bool InpOnePositionFlat = true; + +input group "Thống kê (góc dưới-trái chart)" +input bool InpShowStats = true; +input int InpStatFontSize = 9; +input color InpStatColor = clrSilver; + //--- Buffers & state (trùng State.mqh) double buf_RSI[]; double buf_EMA9[]; @@ -61,13 +79,45 @@ const string LBL_RSI_VAL = OBJ_PREFIX + "rsi"; const string LBL_EMA9VAL = OBJ_PREFIX + "ema9"; const string LBL_WMA45VAL = OBJ_PREFIX + "wma45"; +const string STAT_PREFIX = "RsiMomEA_ST_"; +const string STAT_L1 = STAT_PREFIX + "line1"; +const string STAT_L2 = STAT_PREFIX + "line2"; +const string STAT_L3 = STAT_PREFIX + "line3"; +// Khoảng cách dọc giữa các dòng thống kê (pixel): bước = fontSize + STAT_LINE_PAD +const int STAT_Y_ANCHOR = 18; +const int STAT_LINE_PAD = 16; + +long g_statExitDeals = 0; +long g_statSL = 0; +long g_statTP = 0; +long g_statOther = 0; +long g_statWins = 0; +double g_statSumProfit = 0.0; + datetime g_lastAlertBuyBar = 0; datetime g_lastAlertSellBar = 0; bool g_firstCalc = true; static int g_prevCalculated = 0; +datetime g_tradeBarAnchor = 0; + +CTrade g_trade; + long ActChart() { return ChartID(); } +void SetTradeFillingFromSymbol(); +bool NearestSwingSlTp(const bool isBuy, const double entry, const int dig, double &sl, double &tp); +bool StopsValid(const bool isBuy, const double price, const double sl, const double tp); +int CountMyMagicPositions(); +double NormalizeLots(double v); +double VolumeForRiskPercent(const bool isBuy, const double entryRef, const double slPrice); +void TradeTryOnBarOpen(const int calcRet); +void Stats_CreateObjects(); +void Stats_UpdateDisplay(); +void OnTradeTransaction(const MqlTradeTransaction &trans, + const MqlTradeRequest &request, + const MqlTradeResult &result); + //+------------------------------------------------------------------+ bool Handles_CreateAll() { @@ -143,8 +193,8 @@ void Panel_CreateAll() CreateLabel(LBL_TITLE, "─ RSI MOMENTUM (EA) ─", clrWhite, 10, 14, 10); CreateLabel(LBL_TREND, "Trend : ---", clrSilver, 10, 34, 9); CreateLabel(LBL_RSI_VAL, "RSI : ---", clrMediumOrchid, 10, 51, 9); - CreateLabel(LBL_EMA9VAL, "EMA9 : ---", clrDarkOrange, 10, 68, 9); - CreateLabel(LBL_WMA45VAL, "WMA45 : ---", clrDodgerBlue, 10, 85, 9); + CreateLabel(LBL_EMA9VAL, "EMA9 : ---", InpPanelColorEMA9, 10, 68, 9); + CreateLabel(LBL_WMA45VAL, "WMA45 : ---", InpPanelColorWMA45, 10, 85, 9); } //+------------------------------------------------------------------+ @@ -173,8 +223,8 @@ void Panel_Update(const double &closeArr[], const double &ema200Arr[], UpdateLabel(LBL_TREND, trendTxt, trendClr); UpdateLabel(LBL_RSI_VAL, StringFormat("RSI : %6.2f", buf_RSI[1]), clrMediumOrchid); - UpdateLabel(LBL_EMA9VAL, StringFormat("EMA9 : %6.2f", buf_EMA9[1]), clrDarkOrange); - UpdateLabel(LBL_WMA45VAL, StringFormat("WMA45 : %6.2f", buf_WMA45[1]), clrDodgerBlue); + UpdateLabel(LBL_EMA9VAL, StringFormat("EMA9 : %6.2f", buf_EMA9[1]), InpPanelColorEMA9); + UpdateLabel(LBL_WMA45VAL, StringFormat("WMA45 : %6.2f", buf_WMA45[1]), InpPanelColorWMA45); } //+------------------------------------------------------------------+ @@ -462,12 +512,26 @@ int OnInit() g_lastAlertSellBar = 0; g_firstCalc = true; g_prevCalculated = 0; + g_tradeBarAnchor = iTime(_Symbol, _Period, 0); + + g_statExitDeals = 0; + g_statSL = 0; + g_statTP = 0; + g_statOther = 0; + g_statWins = 0; + g_statSumProfit = 0.0; + + g_trade.SetExpertMagicNumber(InpMagic); + g_trade.SetDeviationInPoints(InpSlippagePoints); + SetTradeFillingFromSymbol(); if (!Handles_CreateAll()) return INIT_FAILED; Panel_CreateAll(); - Print("[RsiMomEA] Init OK — logic nhúng, prefix object: ", OBJ_PREFIX); + Stats_CreateObjects(); + Stats_UpdateDisplay(); + Print("[RsiMomEA] Init OK — trade=", InpTradeEnabled ? "on" : "off", " risk%=", InpRiskPercent); return INIT_SUCCEEDED; } @@ -476,6 +540,7 @@ void OnDeinit(const int reason) { Handles_ReleaseAll(); ObjectsDeleteAll(ActChart(), OBJ_PREFIX); + ObjectsDeleteAll(ActChart(), STAT_PREFIX); ChartRedraw(ActChart()); } @@ -486,6 +551,349 @@ void OnTick() const int ret = RsiMomentum_OnCalculate(rates_total, g_prevCalculated); if (ret != 0) g_prevCalculated = ret; + + const datetime t0 = iTime(_Symbol, _Period, 0); + if (t0 != 0 && t0 != g_tradeBarAnchor) + { + g_tradeBarAnchor = t0; + TradeTryOnBarOpen(ret); + } +} + +//+------------------------------------------------------------------+ +void TradeTryOnBarOpen(const int calcRet) +{ + if (!InpTradeEnabled) + return; + if (!MQLInfoInteger(MQL_TESTER) && !TerminalInfoInteger(TERMINAL_TRADE_ALLOWED)) + return; + if (calcRet <= 0) + return; + if (ArraySize(buf_Signal) < 2) + return; + + const double s = buf_Signal[1]; + if (s > -0.5 && s < 0.5) + return; + + const bool isBuy = (s > 0.5); + + if (InpOnePositionFlat && CountMyMagicPositions() > 0) + return; + + MqlTick tk; + if (!SymbolInfoTick(_Symbol, tk)) + return; + + const int dig = (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS); + const double entry = isBuy ? tk.ask : tk.bid; + double sl = 0.0, tp = 0.0; + + if (!NearestSwingSlTp(isBuy, entry, dig, sl, tp)) + { + Print("[RsiMomEA] Trade skip: SL/TP swing không hợp lệ"); + return; + } + if (!StopsValid(isBuy, entry, sl, tp)) + { + Print("[RsiMomEA] Trade skip: STOPS_LEVEL / FREEZE"); + return; + } + + const double balance = AccountInfoDouble(ACCOUNT_BALANCE); + const double riskMoney = balance * (InpRiskPercent / 100.0); + double vol = VolumeForRiskPercent(isBuy, entry, sl); + vol = NormalizeLots(vol); + if (vol <= 0.0) + { + Print("[RsiMomEA] Trade skip: volume=0"); + return; + } + + const bool ok = isBuy + ? g_trade.Buy(vol, _Symbol, tk.ask, sl, tp, "RsiMom BUY") + : g_trade.Sell(vol, _Symbol, tk.bid, sl, tp, "RsiMom SELL"); + + if (!ok) + Print("[RsiMomEA] Order fail ", g_trade.ResultRetcode(), " ", g_trade.ResultComment()); + else + Print("[RsiMomEA] Order OK #", g_trade.ResultOrder(), " ", isBuy ? "BUY" : "SELL", + " vol=", vol, " SL=", DoubleToString(sl, dig), " TP=", DoubleToString(tp, dig)); +} + +//+------------------------------------------------------------------+ +bool NearestSwingSlTp(const bool isBuy, const double entry, const int dig, double &sl, double &tp) +{ + const double rr = MathMax(0.01, InpRewardRiskRatio); + const int mx = MathMax(5, InpSwingMaxBars); + const int spr = (int)SymbolInfoInteger(_Symbol, SYMBOL_SPREAD); + const double buf = spr * _Point; + + if (isBuy) + { + double pivotLow = 0.0; + bool found = false; + for (int i = 2; i <= mx; i++) + { + const double L = iLow(_Symbol, _Period, i); + if (L < iLow(_Symbol, _Period, i - 1) && L < iLow(_Symbol, _Period, i + 1)) + { + pivotLow = L; + found = true; + break; + } + } + if (!found) + { + pivotLow = iLow(_Symbol, _Period, 2); + for (int j = 3; j <= mx; j++) + pivotLow = MathMin(pivotLow, iLow(_Symbol, _Period, j)); + } + sl = NormalizeDouble(pivotLow - buf, dig); + const double risk = entry - sl; + if (risk <= _Point * 2) + return false; + tp = NormalizeDouble(entry + risk * rr, dig); + } + else + { + double pivotHigh = 0.0; + bool found = false; + for (int i = 2; i <= mx; i++) + { + const double H = iHigh(_Symbol, _Period, i); + if (H > iHigh(_Symbol, _Period, i - 1) && H > iHigh(_Symbol, _Period, i + 1)) + { + pivotHigh = H; + found = true; + break; + } + } + if (!found) + { + pivotHigh = iHigh(_Symbol, _Period, 2); + for (int j = 3; j <= mx; j++) + pivotHigh = MathMax(pivotHigh, iHigh(_Symbol, _Period, j)); + } + sl = NormalizeDouble(pivotHigh + buf, dig); + const double risk = sl - entry; + if (risk <= _Point * 2) + return false; + tp = NormalizeDouble(entry - risk * rr, dig); + } + return true; +} + +//+------------------------------------------------------------------+ +bool StopsValid(const bool isBuy, const double price, const double sl, const double tp) +{ + const int stops = (int)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL); + const int freeze = (int)SymbolInfoInteger(_Symbol, SYMBOL_TRADE_FREEZE_LEVEL); + const double md = (stops > freeze ? stops : freeze) * _Point; + if (md <= 0.0) + return true; + + if (isBuy) + { + if (price - sl < md - _Point) return false; + if (tp - price < md - _Point) return false; + } + else + { + if (sl - price < md - _Point) return false; + if (price - tp < md - _Point) return false; + } + return true; +} + +//+------------------------------------------------------------------+ +int CountMyMagicPositions() +{ + int n = 0; + for (int i = PositionsTotal() - 1; i >= 0; i--) + { + if (!PositionGetTicket(i)) + continue; + if (PositionGetString(POSITION_SYMBOL) != _Symbol) + continue; + if ((ulong)PositionGetInteger(POSITION_MAGIC) != InpMagic) + continue; + n++; + } + return n; +} + +//+------------------------------------------------------------------+ +double VolumeForRiskPercent(const bool isBuy, const double entryRef, const double slPrice) +{ + if (MathAbs(entryRef - slPrice) < _Point) + return 0.0; + + const double balance = AccountInfoDouble(ACCOUNT_BALANCE); + const double riskMoney = balance * (InpRiskPercent / 100.0); + + double profit = 0.0; + if (!OrderCalcProfit(isBuy ? ORDER_TYPE_BUY : ORDER_TYPE_SELL, + _Symbol, 1.0, entryRef, slPrice, profit)) + return 0.0; + + const double lossPerLot = MathAbs(profit); + if (lossPerLot < DBL_EPSILON) + return 0.0; + + return riskMoney / lossPerLot; +} + +//+------------------------------------------------------------------+ +double NormalizeLots(double v) +{ + const double step = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP); + const double vmin = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN); + const double vmax = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX); + if (step <= 0.0) + return 0.0; + v = MathFloor(v / step) * step; + if (v < vmin - 1e-12) + return 0.0; + if (v > vmax) + v = vmax; + return NormalizeDouble(v, 8); +} + +//+------------------------------------------------------------------+ +void SetTradeFillingFromSymbol() +{ + const long fm = SymbolInfoInteger(_Symbol, SYMBOL_FILLING_MODE); + if ((fm & SYMBOL_FILLING_IOC) != 0) + g_trade.SetTypeFilling(ORDER_FILLING_IOC); + else if ((fm & SYMBOL_FILLING_FOK) != 0) + g_trade.SetTypeFilling(ORDER_FILLING_FOK); + else + g_trade.SetTypeFilling(ORDER_FILLING_RETURN); +} + +//+------------------------------------------------------------------+ +void Stats_CreateObjects() +{ + const long ch = ActChart(); + const int fs = MathMax(7, InpStatFontSize); + const int step = fs + STAT_LINE_PAD; + + for (int k = 0; k < 3; k++) + { + const string name = (k == 0) ? STAT_L1 : ((k == 1) ? STAT_L2 : STAT_L3); + if (ObjectFind(ch, name) >= 0) + continue; + ObjectCreate(ch, name, OBJ_LABEL, 0, 0, 0); + ObjectSetInteger(ch, name, OBJPROP_CORNER, CORNER_LEFT_LOWER); + ObjectSetInteger(ch, name, OBJPROP_XDISTANCE, 8); + ObjectSetInteger(ch, name, OBJPROP_YDISTANCE, STAT_Y_ANCHOR + k * step); + ObjectSetInteger(ch, name, OBJPROP_FONTSIZE, fs); + ObjectSetString (ch, name, OBJPROP_FONT, "Consolas"); + ObjectSetInteger(ch, name, OBJPROP_COLOR, InpStatColor); + ObjectSetInteger(ch, name, OBJPROP_BACK, false); + ObjectSetInteger(ch, name, OBJPROP_SELECTABLE, false); + ObjectSetInteger(ch, name, OBJPROP_HIDDEN, false); + ObjectSetString (ch, name, OBJPROP_TEXT, ""); + } +} + +//+------------------------------------------------------------------+ +void Stats_UpdateDisplay() +{ + const long ch = ActChart(); + + if (ObjectFind(ch, STAT_L1) < 0) + return; + + if (!InpShowStats) + { + ObjectSetString(ch, STAT_L1, OBJPROP_TEXT, ""); + ObjectSetString(ch, STAT_L2, OBJPROP_TEXT, ""); + ObjectSetString(ch, STAT_L3, OBJPROP_TEXT, ""); + ChartRedraw(ch); + return; + } + + const int fs = MathMax(7, InpStatFontSize); + const int step = fs + STAT_LINE_PAD; + for (int k = 0; k < 3; k++) + { + const string nm = (k == 0) ? STAT_L1 : ((k == 1) ? STAT_L2 : STAT_L3); + ObjectSetInteger(ch, nm, OBJPROP_FONTSIZE, fs); + ObjectSetInteger(ch, nm, OBJPROP_YDISTANCE, STAT_Y_ANCHOR + k * step); + } + + string line1 = StringFormat("Total: %I64d | SL %I64d | TP %I64d", + g_statExitDeals, g_statSL, g_statTP); + if (g_statOther > 0) + line1 += StringFormat(" | Other %I64d", g_statOther); + + double winrate = 0.0; + if (g_statExitDeals > 0) + winrate = 100.0 * (double)g_statWins / (double)g_statExitDeals; + + const string cur = AccountInfoString(ACCOUNT_CURRENCY); + double avg = 0.0; + if (g_statExitDeals > 0) + avg = g_statSumProfit / (double)g_statExitDeals; + + const string line2 = StringFormat("Winrate: %.1f%%", winrate); + const string line3 = StringFormat("Average Profit / trade: %s %s", + DoubleToString(avg, 2), cur); + + ObjectSetString (ch, STAT_L1, OBJPROP_TEXT, line1); + ObjectSetInteger(ch, STAT_L1, OBJPROP_COLOR, InpStatColor); + ObjectSetString (ch, STAT_L2, OBJPROP_TEXT, line2); + ObjectSetInteger(ch, STAT_L2, OBJPROP_COLOR, InpStatColor); + ObjectSetString (ch, STAT_L3, OBJPROP_TEXT, line3); + ObjectSetInteger(ch, STAT_L3, OBJPROP_COLOR, InpStatColor); + ChartRedraw(ch); +} + +//+------------------------------------------------------------------+ +void OnTradeTransaction(const MqlTradeTransaction &trans, + const MqlTradeRequest &request, + const MqlTradeResult &result) +{ + if (trans.type != TRADE_TRANSACTION_DEAL_ADD) + return; + + const ulong dealTicket = trans.deal; + if (dealTicket == 0) + return; + + if (!HistoryDealSelect(dealTicket)) + return; + + if (HistoryDealGetString(dealTicket, DEAL_SYMBOL) != _Symbol) + return; + if ((ulong)HistoryDealGetInteger(dealTicket, DEAL_MAGIC) != InpMagic) + return; + + const long entry = HistoryDealGetInteger(dealTicket, DEAL_ENTRY); + if (entry != DEAL_ENTRY_OUT) + return; + + const double profit = HistoryDealGetDouble(dealTicket, DEAL_PROFIT) + + HistoryDealGetDouble(dealTicket, DEAL_SWAP) + + HistoryDealGetDouble(dealTicket, DEAL_COMMISSION); + + const ENUM_DEAL_REASON reason = (ENUM_DEAL_REASON)HistoryDealGetInteger(dealTicket, DEAL_REASON); + + g_statExitDeals++; + g_statSumProfit += profit; + if (profit > 0.0) + g_statWins++; + + if (reason == DEAL_REASON_SL) + g_statSL++; + else if (reason == DEAL_REASON_TP) + g_statTP++; + else + g_statOther++; + + Stats_UpdateDisplay(); } //+------------------------------------------------------------------+