diff --git a/Experts/MQL5.code-workspace b/Experts/MQL5.code-workspace new file mode 100644 index 0000000..b7ccd61 --- /dev/null +++ b/Experts/MQL5.code-workspace @@ -0,0 +1,17 @@ +{ + "folders": [ + { + "path": ".." + }, + { + "path": "D:/Coding Tools" + } + ], + "settings": { + "files.associations": { + "*.mqh": "cpp", + "*.mq4": "cpp", + "*.mq5": "cpp" + } + } +} \ No newline at end of file diff --git a/Experts/RSIForceStateEA.mq5 b/Experts/RSIForceStateEA.mq5 new file mode 100644 index 0000000..0f6d157 --- /dev/null +++ b/Experts/RSIForceStateEA.mq5 @@ -0,0 +1,159 @@ +//+------------------------------------------------------------------+ +//| RSIForceStateEA - pullback by RSI/EMA9/WMA45 force, EMA200 trend | +//| State machine: NO_TRADE -> WATCHING -> PENDING_ORDER -> IN_TRADE | +//+------------------------------------------------------------------+ +#property copyright "RSI Force State EA" +#property version "1.30" +#property strict + +// ---- Layer 1: inputs + value types + indicator buffers/handles ---- +#include "RSIForceStateEA/Config.mqh" +#include "RSIForceStateEA/State.mqh" +#include "RSIForceStateEA/Indicators.mqh" + +// ---- Globals consumed by Layer 2/3 modules ---- +EAState g_State = STATE_NO_TRADE; +PendingContext g_Pending; +TradeContext g_OpenTrade; + +// ---- Layer 2: trade ops + state machine (depend on globals above) ---- +#include "RSIForceStateEA/Trade.mqh" +#include "RSIForceStateEA/StateMachine.mqh" + +// ---- Layer 3: visualization (depends on globals + state machine) ---- +#include "RSIForceStateEA/Visualizer.mqh" + +// Throttle stats panel refresh (deal history scan can be heavy). +datetime g_LastVisualRefresh = 0; +const int kVisualRefreshSeconds = 2; + +// ------------------------------------------------------------------ +// Input validation (fail fast on misconfiguration) +// ------------------------------------------------------------------ + +bool ValidateInputs() +{ + if (InpRiskPercent <= 0.0 || InpRiskPercent > 50.0) + { Print("[INIT] InpRiskPercent must be in (0, 50]"); return false; } + + if (InpRiskRewardRatio <= 0.0) + { Print("[INIT] InpRiskRewardRatio must be > 0"); return false; } + + if (InpPartialCloseAtR <= 0.0 || InpPartialCloseAtR >= InpRiskRewardRatio) + { Print("[INIT] InpPartialCloseAtR must be in (0, InpRiskRewardRatio)"); return false; } + + if (InpPartialClosePercent <= 0.0 || InpPartialClosePercent >= 100.0) + { Print("[INIT] InpPartialClosePercent must be in (0, 100)"); return false; } + + if (InpPendingMaxAliveBars < 1) + { Print("[INIT] InpPendingMaxAliveBars must be >= 1"); return false; } + + if (InpWatchingMaxBars < 1) + { Print("[INIT] InpWatchingMaxBars must be >= 1"); return false; } + + if (InpSlopeLookbackBars < 2) + { Print("[INIT] InpSlopeLookbackBars must be >= 2"); return false; } + + if (InpMinBarsBetweenCrosses < 1) + { Print("[INIT] InpMinBarsBetweenCrosses must be >= 1"); return false; } + + if (InpSwingLookbackBars < 5) + { Print("[INIT] InpSwingLookbackBars must be >= 5"); return false; } + + if (InpSidewayRSILow >= InpSidewayRSIHigh) + { Print("[INIT] InpSidewayRSILow must be < InpSidewayRSIHigh"); return false; } + + if (InpRSI_EMA9Period >= InpRSI_WMA45Period) + { Print("[INIT] InpRSI_EMA9Period must be < InpRSI_WMA45Period"); return false; } + + if (InpSignalBarShift < 1) + { Print("[INIT] InpSignalBarShift must be >= 1 (use closed bars)"); return false; } + + return true; +} + +// ------------------------------------------------------------------ +// Lifecycle +// ------------------------------------------------------------------ + +int OnInit() +{ + if (!ValidateInputs()) return INIT_PARAMETERS_INCORRECT; + + ZeroMemory(g_Pending); + ZeroMemory(g_OpenTrade); + g_State = STATE_NO_TRADE; + g_HasTradedThisPullback = false; + g_BarsInWatching = 0; + g_LastTrend = TREND_NONE; + g_LastDirTrend = TREND_NONE; + g_LastVisualRefresh = 0; + + if (!InitIndicators()) + { + Print("[INIT] InitIndicators failed"); + return INIT_FAILED; + } + if (!RefreshIndicatorData()) + { + Print("[INIT] RefreshIndicatorData failed (need more history)"); + return INIT_FAILED; + } + + InitTradeOps(); + + // Seed both trend trackers to current value so we don't fire a false + // "direction flip" event on the first tick. + g_LastTrend = DetectTrend(InpSignalBarShift); + if (g_LastTrend != TREND_NONE) g_LastDirTrend = g_LastTrend; + + PrintFormat("[INIT] RSIForceStateEA v1.30 ready | Symbol=%s | TF=%d | startTrend=%s", + _Symbol, (int)_Period, EnumToString(g_LastTrend)); + + AttachIndicatorsToChart(); + DrawAllVisuals(g_LastTrend); + return INIT_SUCCEEDED; +} + +void OnDeinit(const int reason) +{ + RemoveAllVisuals(); + ReleaseIndicators(); + PrintFormat("[DEINIT] reason=%d", reason); +} + +// ------------------------------------------------------------------ +// Tick loop: +// - tick-level: refresh data, sync broker state, manage open trade +// - bar-level: evaluate state machine on every newly closed bar +// - visuals: refresh dashboard/levels (throttled) +// ------------------------------------------------------------------ +void OnTick() +{ + if (!RefreshIndicatorData()) return; + + SyncStateWithBroker(); + + if (g_State == STATE_IN_TRADE) + ManagePartialAndBreakEven(g_OpenTrade); + + const bool newBar = IsNewBar(); + if (newBar) RunStateMachine(); + + // Throttle the dashboard refresh to avoid excessive history queries. + const datetime now = TimeCurrent(); + if (newBar || (now - g_LastVisualRefresh) >= kVisualRefreshSeconds) + { + g_LastVisualRefresh = now; + DrawAllVisuals(DetectTrend(InpSignalBarShift)); + } +} + +// Trade events can change order/position state between ticks; just resync. +// Visuals will refresh on the next tick (throttled by kVisualRefreshSeconds). +void OnTradeTransaction(const MqlTradeTransaction &trans, + const MqlTradeRequest &request, + const MqlTradeResult &result) +{ + SyncStateWithBroker(); +} diff --git a/Experts/RSIForceStateEA/Config.mqh b/Experts/RSIForceStateEA/Config.mqh new file mode 100644 index 0000000..ffdfe20 --- /dev/null +++ b/Experts/RSIForceStateEA/Config.mqh @@ -0,0 +1,74 @@ +#ifndef RSI_FORCE_STATE_EA__CONFIG_MQH +#define RSI_FORCE_STATE_EA__CONFIG_MQH + +// ============================================================ +// All EA inputs grouped by responsibility for clarity in MT5 UI +// ============================================================ + +input group "General" +input long InpMagicNumber = 26050901; +input bool InpDebugLog = true; + +input group "Trend Filter (EMA200 on Close)" +input int InpEMA200Period = 200; +input bool InpUseATRTrendBuffer = false; // false = % buffer, true = ATR buffer +input double InpTrendBufferPercent = 0.10; // % EMA200 dead-zone (when ATR buffer off) +input double InpTrendBufferATRMult = 0.20; // ATR multiplier for dead-zone (when ATR buffer on) +input bool InpSkipFlatEMA200 = true; +input double InpFlatEMA_ATRMult = 0.05; // EMA200 considered flat if |delta| < ATR*this + +input group "ATR (used by trend buffer + SL)" +input int InpATRPeriod = 14; + +input group "Force Indicators (RSI + 2 MAs computed on RSI)" +input int InpRSIPeriod = 14; +input int InpRSI_EMA9Period = 9; // EMA tinh tren RSI +input int InpRSI_WMA45Period = 45; // WMA tinh tren RSI +input int InpSlopeLookbackBars = 3; // bars de check slope dong nhat +input int InpMinBarsBetweenCrosses = 10; // toi thieu N bar khong cross truoc khi nhan tin hieu + +input group "Sideway / No-Trade Filters" +input bool InpUseRSISidewayFilter = true; +input int InpSidewayLookbackBars = 8; +input double InpSidewayRSILow = 45.0; +input double InpSidewayRSIHigh = 55.0; + +input group "Entry & Pending Order" +input int InpSignalBarShift = 1; // 1 = nen vua dong (khuyen nghi) +input int InpPendingMaxAliveBars = 5; // huy pending sau N bar khong khop +input int InpWatchingMaxBars = 10; // huy WATCHING neu khong co trigger sau N bar +input bool InpInvalidateIfCrossBack = true; // huy pending khi RSI cross nguoc lai + +input group "Stop Loss" +enum ENUM_SL_MODE +{ + SL_SWING = 0, // theo swing extreme gan nhat + SL_ATR = 1, // theo ATR + SL_HYBRID = 2 // chon SL rong hon giua swing va ATR +}; +input ENUM_SL_MODE InpStopLossMode = SL_HYBRID; +input int InpSwingLookbackBars = 20; +input double InpSL_ATRMult = 1.2; +input int InpSL_SwingBufferPoints = 20; // them buffer ngoai swing extreme + +input group "Risk & Trade Management" +input double InpRiskPercent = 1.0; +input double InpRiskRewardRatio = 2.0; // TP = entry +/- R*RR +input double InpPartialCloseAtR = 1.5; // dong 1 phan khi dat R nay +input double InpPartialClosePercent = 50.0; // phan tram dong khi dat partial R + +input group "Visualization" +input bool InpVisualize = true; // bat tat toan bo overlay +input bool InpAttachIndicators = true; // tu add EMA200 vao chart + RSI/EMA9/WMA45 vao subwindow +input bool InpShowDashboard = true; // panel goc tren-trai +input bool InpShowStatsPanel = true; // panel goc duoi-trai +input bool InpShowTradeLevels = true; // ve Entry/SL/TP TradingView style +input int InpStatsLookbackDays = 60; // chi quet history N ngay gan nhat cho stats +input color InpColorTrendUp = clrLime; +input color InpColorTrendDown = clrTomato; +input color InpColorTrendNone = clrSilver; +input color InpColorEntry = clrDodgerBlue; +input color InpColorSL = clrTomato; +input color InpColorTP = clrLime; + +#endif diff --git a/Experts/RSIForceStateEA/Indicators.mqh b/Experts/RSIForceStateEA/Indicators.mqh new file mode 100644 index 0000000..a7bd26d --- /dev/null +++ b/Experts/RSIForceStateEA/Indicators.mqh @@ -0,0 +1,122 @@ +#ifndef RSI_FORCE_STATE_EA__INDICATORS_MQH +#define RSI_FORCE_STATE_EA__INDICATORS_MQH + +// ============================================================ +// Indicator handles + cached series buffers. +// All buffers are timeseries indexed (index 0 = current bar). +// ============================================================ + +int g_hRSI = INVALID_HANDLE; +int g_hEMA9 = INVALID_HANDLE; // EMA(InpRSI_EMA9Period) computed on RSI +int g_hWMA45 = INVALID_HANDLE; // WMA(InpRSI_WMA45Period) computed on RSI +int g_hEMA200 = INVALID_HANDLE; // EMA(InpEMA200Period) on close +int g_hATR = INVALID_HANDLE; + +// Dynamic arrays so ArraySetAsSeries(...) is allowed. +double g_RSI[]; +double g_EMA9[]; +double g_WMA45[]; +double g_EMA200[]; +double g_ATR[]; +MqlRates g_Bars[]; + +// How many bars we keep cached. Must be > swing/sideway/cross lookback. +const int kIndicatorCacheBars = 200; + +// ------------------------------------------------------------ +// Lifecycle +// ------------------------------------------------------------ + +bool InitIndicators() +{ + g_hRSI = iRSI(_Symbol, _Period, InpRSIPeriod, PRICE_CLOSE); + if (g_hRSI == INVALID_HANDLE) return false; + + // EMA9 / WMA45 are computed on the RSI buffer (not on price). + g_hEMA9 = iMA(_Symbol, _Period, InpRSI_EMA9Period, 0, MODE_EMA, g_hRSI); + if (g_hEMA9 == INVALID_HANDLE) return false; + + g_hWMA45 = iMA(_Symbol, _Period, InpRSI_WMA45Period, 0, MODE_LWMA, g_hRSI); + if (g_hWMA45 == INVALID_HANDLE) return false; + + g_hEMA200 = iMA(_Symbol, _Period, InpEMA200Period, 0, MODE_EMA, PRICE_CLOSE); + if (g_hEMA200 == INVALID_HANDLE) return false; + + g_hATR = iATR(_Symbol, _Period, InpATRPeriod); + if (g_hATR == INVALID_HANDLE) return false; + + ArraySetAsSeries(g_RSI, true); + ArraySetAsSeries(g_EMA9, true); + ArraySetAsSeries(g_WMA45, true); + ArraySetAsSeries(g_EMA200, true); + ArraySetAsSeries(g_ATR, true); + ArraySetAsSeries(g_Bars, true); + return true; +} + +void ReleaseIndicators() +{ + if (g_hRSI != INVALID_HANDLE) IndicatorRelease(g_hRSI); + if (g_hEMA9 != INVALID_HANDLE) IndicatorRelease(g_hEMA9); + if (g_hWMA45 != INVALID_HANDLE) IndicatorRelease(g_hWMA45); + if (g_hEMA200 != INVALID_HANDLE) IndicatorRelease(g_hEMA200); + if (g_hATR != INVALID_HANDLE) IndicatorRelease(g_hATR); +} + +// Refresh the cached series buffers; returns false on partial copy. +bool RefreshIndicatorData() +{ + const int n = kIndicatorCacheBars; + if (CopyRates(_Symbol, _Period, 0, n, g_Bars) < n) return false; + if (CopyBuffer(g_hRSI, 0, 0, n, g_RSI) < n) return false; + if (CopyBuffer(g_hEMA9, 0, 0, n, g_EMA9) < n) return false; + if (CopyBuffer(g_hWMA45, 0, 0, n, g_WMA45) < n) return false; + if (CopyBuffer(g_hEMA200, 0, 0, n, g_EMA200) < n) return false; + if (CopyBuffer(g_hATR, 0, 0, n, g_ATR) < n) return false; + return true; +} + +// ------------------------------------------------------------ +// Helpers used by the state machine on series-indexed buffers +// ------------------------------------------------------------ + +// True when buffer is monotonically decreasing across [shift .. shift+lookback-1]. +// In timeseries indexing: newer bar = lower index, so "down slope" means +// older value > newer value, i.e. buffer[i+1] > buffer[i]. +bool IsBufferSlopingDown(const double &buffer[], const int shift, const int lookback) +{ + for (int i = shift; i < shift + lookback - 1; i++) + { + if (!(buffer[i] < buffer[i + 1])) return false; + } + return true; +} + +// True when buffer is monotonically increasing across [shift .. shift+lookback-1]. +bool IsBufferSlopingUp(const double &buffer[], const int shift, const int lookback) +{ + for (int i = shift; i < shift + lookback - 1; i++) + { + if (!(buffer[i] > buffer[i + 1])) return false; + } + return true; +} + +// Returns true if (a-b) sign changes anywhere within +// [fromShift .. fromShift+lookback-1]. Used to ensure RSI has stayed +// on one side of WMA45 for at least N bars before a fresh trigger. +bool HasCrossInLastNBars(const double &a[], const double &b[], const int fromShift, const int lookback) +{ + for (int i = fromShift; i < fromShift + lookback; i++) + { + const double diffNew = a[i] - b[i]; + const double diffOld = a[i + 1] - b[i + 1]; + if (diffNew == 0.0 || diffOld == 0.0 + || (diffNew > 0.0 && diffOld < 0.0) + || (diffNew < 0.0 && diffOld > 0.0)) + return true; + } + return false; +} + +#endif diff --git a/Experts/RSIForceStateEA/README.md b/Experts/RSIForceStateEA/README.md new file mode 100644 index 0000000..91e49d8 --- /dev/null +++ b/Experts/RSIForceStateEA/README.md @@ -0,0 +1,235 @@ +# RSIForceStateEA + +EA giao dich pullback theo dong luc RSI, loc trend bang EMA200, dieu phoi qua state +machine 4 trang thai. Chi dung 1 lenh moi pullback, vao bang BUY/SELL LIMIT. + +## 1) Cau truc thu muc + +- EA chinh: `Experts/RSIForceStateEA.mq5` +- Cac module: + - `Experts/RSIForceStateEA/Config.mqh` - toan bo input + - `Experts/RSIForceStateEA/State.mqh` - enum + struct (no globals) + - `Experts/RSIForceStateEA/Indicators.mqh` - handle + buffer + helper slope/cross + - `Experts/RSIForceStateEA/Trade.mqh` - dat lenh, sizing, partial + BE + - `Experts/RSIForceStateEA/StateMachine.mqh` - flow 4 state + - `Experts/RSIForceStateEA/Visualizer.mqh` - dashboard, stats panel, trade levels + - `Experts/RSIForceStateEA/README.md` + +Thu tu include trong file `.mq5`: + +``` +Layer 1 : Config -> State -> Indicators +Globals : g_State, g_Pending, g_OpenTrade +Layer 2 : Trade -> StateMachine +Layer 3 : Visualizer +``` + +## 2) Logic tong the + +### 2.1 Cac chi bao + +- `RSI(InpRSIPeriod)` - dong luc gia. +- `EMA(InpRSI_EMA9Period)` tinh tren RSI. +- `WMA(InpRSI_WMA45Period)` tinh tren RSI. +- `EMA(InpEMA200Period)` tren close - loc trend. +- `ATR(InpATRPeriod)` - dung cho trend buffer va SL. + +### 2.2 Trend filter + +- `Uptrend` khi `Close > EMA200 + buffer`. +- `Downtrend` khi `Close < EMA200 - buffer`. +- `buffer`: + - `% EMA200` mac dinh (`InpTrendBufferPercent = 0.10%`), hoac + - `ATR-based` neu `InpUseATRTrendBuffer = true`. +- `InpSkipFlatEMA200`: bo qua khi EMA200 phang (delta giua 2 nen <= `ATR * InpFlatEMA_ATRMult`). + +### 2.3 4 trang thai + + +| State | Y nghia | +| --------------------- | -------------------------------------------------- | +| `STATE_NO_TRADE` | khong co setup hop le | +| `STATE_WATCHING` | da co pullback hop le, dang cho trigger | +| `STATE_PENDING_ORDER` | da dat BUY/SELL LIMIT, dang cho khop | +| `STATE_IN_TRADE` | da khop, dang quan ly position (partial + BE + TP) | + + +### 2.4 Pullback (NO_TRADE -> WATCHING) + +- Uptrend pullback: + - `RSI < EMA9 < WMA45` + - 3 buffer cung slope giam (kiem tra `InpSlopeLookbackBars` nen) +- Downtrend pullback (mirror). + +### 2.5 Trigger entry (WATCHING -> PENDING_ORDER) + +- BUY trigger: + - RSI cat len WMA45 (so sanh nen `InpSignalBarShift` voi nen truoc). + - EMA9 van con < WMA45 (xac nhan dong luc moi bat). + - Khong co cross RSI/WMA45 trong `InpMinBarsBetweenCrosses` nen truoc do + (tin hieu phai "isolated", tranh nhieu sat WMA45). +- SELL trigger (mirror). + +### 2.6 Dat LIMIT ORDER + +- `Entry = (Close(signal) + SwingExtreme) / 2` + - BUY: SwingExtreme = swing low gan nhat trong `InpSwingLookbackBars`. + - SELL: SwingExtreme = swing high gan nhat. +- Lenh dat la `BuyLimit` / `SellLimit` (khong co market chase). +- Pending song toi da `InpPendingMaxAliveBars`. Het han -> huy. +- Neu `InpInvalidateIfCrossBack = true`, khi RSI cross nguoc lai WMA45 +hoac trend mat -> huy ngay. +- WATCHING song toi da `InpWatchingMaxBars`. Het han -> tro ve NO_TRADE. + +### 2.7 Stop Loss + + +| Mode | Cong thuc | +| ----------- | -------------------------------------------------------------- | +| `SL_SWING` | swing extreme +/- `InpSL_SwingBufferPoints` | +| `SL_ATR` | `Entry +/- ATR * InpSL_ATRMult` | +| `SL_HYBRID` | chon SL **rong hon** giua swing va ATR (an toan hon, mac dinh) | + + +### 2.8 Take Profit + +- `TP = Entry +/- R * InpRiskRewardRatio` (mac dinh `2R`). + +### 2.9 Quan ly trong IN_TRADE + +- Risk = `InpRiskPercent` (% balance, mac dinh 1%). +- Khi gia chay duoc `InpPartialCloseAtR` (mac dinh `1.5R`): + - Dong `InpPartialClosePercent`% volume (mac dinh 50%). + - Doi SL ve `Entry` (BE). +- Neu volume khong the chia (volMin chan), van dich SL ve BE va bo qua partial. +- Khong lam gi them o `1R`. + +### 2.10 Anti-spam + +- Mot `pullback` chi sinh ra 1 lenh (`g_HasTradedThisPullback`). +- Reset bookkeeping khi trend regime doi (TREND_UP <-> TREND_DOWN/NONE). +- Khi trend doi va dang co pending nguoc huong -> huy luon. + +### 2.11 Sideway filter + +- `InpUseRSISidewayFilter`: khi tat ca `InpSidewayLookbackBars` gia tri RSI gan nhat +nam trong `[InpSidewayRSILow, InpSidewayRSIHigh]` -> bo qua tim setup moi. +- Filter nay chi chan `NO_TRADE -> WATCHING`, khong chan vong doi pending va +khong chan quan ly position. + +## 3) Cach build va chay + +1. Mo MetaEditor. +2. Mo `MQL5/Experts/RSIForceStateEA.mq5` va Compile (F7). +3. Quay lai MT5, attach EA vao chart muon trade. +4. Bat `Algo Trading`. +5. Inputs co the dieu chinh ngay tu UI khi attach. + +## 4) Cac input quan trong (mac dinh) + + +| Input | Mac dinh | Mo ta | +| -------------------------- | --------- | --------------------------------------- | +| `InpMagicNumber` | 26050901 | Magic, doi neu chay nhieu instance | +| `InpRiskPercent` | 1.0 | % balance/lenh | +| `InpRiskRewardRatio` | 2.0 | TP = R * day | +| `InpPartialCloseAtR` | 1.5 | dong 1 phan tai R nay | +| `InpPartialClosePercent` | 50.0 | % volume dong tai partial | +| `InpPendingMaxAliveBars` | 5 | huy pending sau N nen | +| `InpWatchingMaxBars` | 10 | huy WATCHING neu khong trigger | +| `InpMinBarsBetweenCrosses` | 10 | dam bao tin hieu cross "isolated" | +| `InpSlopeLookbackBars` | 3 | so nen kiem tra slope | +| `InpSwingLookbackBars` | 20 | tim swing extreme cho entry/SL | +| `InpStopLossMode` | SL_HYBRID | SL_SWING / SL_ATR / SL_HYBRID | +| `InpSL_ATRMult` | 1.2 | ATR multiplier khi SL theo ATR | +| `InpSL_SwingBufferPoints` | 20 | them buffer (point) ngoai swing extreme | +| `InpUseRSISidewayFilter` | true | bat/tat sideway filter | +| `InpSidewayRSILow / High` | 45 / 55 | dai sideway theo RSI | +| `InpUseATRTrendBuffer` | false | doi trend buffer sang dang ATR | + + +## 5) Goi y test va toi uu + +- Backtest tung symbol toi thieu 6-12 thang truoc khi chay live. +- Voi index/forex bien dong cao -> bat `InpUseATRTrendBuffer = true`. +- Muon **it tin hieu hon nhung chat hon**: + - tang `InpMinBarsBetweenCrosses` + - tang `InpSlopeLookbackBars` + - giu `SL_HYBRID` +- Muon **nhieu tin hieu**: + - giam `InpSlopeLookbackBars` ve 2 + - tat `InpUseRSISidewayFilter` +- Theo doi tab `Experts` / `Journal` de xem log `[STATE]`, `[TREND]`, `[PENDING]`, +`[TRADE]`. + +## 6) Visualization + +EA tu hien thi dau day du de quan sat va danh gia: + +### 6.1 Dashboard (goc tren-trai) + +Hien thi cac dong: + +- **Trend** : UP / DOWN / NONE (mau xanh / do / xam) +- **State** : `STATE_NO_TRADE` / `STATE_WATCHING` / `STATE_PENDING_ORDER` / `STATE_IN_TRADE` +- **RSI** : gia tri RSI, EMA9, WMA45 cua nen tin hieu (`InpSignalBarShift`) +- **EMA200**: gia tri EMA200 + close hien tai +- **ATR** : gia tri ATR +- **Context**: tuy state se in: + - `Watching: x/N bars` + - `Pending : dir entry alive=x/N` + - `Trade : dir entry partial=DONE/PEND` + +### 6.2 Stats panel (goc duoi-trai) + +Quet history `InpStatsLookbackDays` ngay (mac dinh 60), filter theo +`InpMagicNumber` + `_Symbol`, hien thi: + +- `Total` : so position da dong (theo POSITION_ID, dedupe partial) +- `TP hit` : so deal dong voi reason `DEAL_REASON_TP` +- `SL hit` : so deal dong voi reason `DEAL_REASON_SL` +- `Other` : dong thu cong / partial close / expert close +- `Net PL` : tong P/L (profit + swap + commission) + +### 6.3 Trade levels (TradingView style) + +Khi state = `PENDING_ORDER` hoac `IN_TRADE`, ve 3 line ngang: + +- `ENTRY` (dotted, `InpColorEntry`) +- `SL` (dashed, `InpColorSL`) +- `TP` (dashed, `InpColorTP`) + +Tu dong xoa khi quay ve `NO_TRADE`. + +### 6.4 Auto-attach indicators + +Khi `InpAttachIndicators = true` (mac dinh): + +- `EMA200` duoc them vao **main chart**. +- `RSI(14)`, `RSI_EMA9`, `RSI_WMA45` duoc them vao **subwindow moi**. + +Co the tat tung phan rieng: + + +| Input | Mac dinh | Tac dung | +| ---------------------- | -------- | ------------------------------------- | +| `InpVisualize` | true | bat tat toan bo overlay | +| `InpAttachIndicators` | true | tu add EMA200 / RSI cluster vao chart | +| `InpShowDashboard` | true | panel goc tren-trai | +| `InpShowStatsPanel` | true | panel goc duoi-trai | +| `InpShowTradeLevels` | true | line Entry/SL/TP | +| `InpStatsLookbackDays` | 60 | so ngay quet stats | + + +Tat ca object visual dung prefix `RSIForce`_ va duoc xoa o `OnDeinit`. + +## 7) Ghi chu kien truc + +- Toan bo state machine xoay quanh **closed bar** (`IsNewBar()`), tranh fire +nhieu lan trong cung 1 nen. +- Quan ly position (partial + BE) chay **moi tick** de phan ung nhanh khi +gia di chuyen. +- `SyncStateWithBroker()` chay moi tick + sau moi `OnTradeTransaction` - +dam bao state nha minh luon khop voi broker (truong hop user dong tay, +pending bi reject, v.v.). + diff --git a/Experts/RSIForceStateEA/State.mqh b/Experts/RSIForceStateEA/State.mqh new file mode 100644 index 0000000..6820685 --- /dev/null +++ b/Experts/RSIForceStateEA/State.mqh @@ -0,0 +1,52 @@ +#ifndef RSI_FORCE_STATE_EA__STATE_MQH +#define RSI_FORCE_STATE_EA__STATE_MQH + +// ============================================================ +// State machine enums + lightweight value types (no globals) +// ============================================================ + +enum EAState +{ + STATE_NO_TRADE = 0, // khong co setup + STATE_WATCHING = 1, // co pullback hop le, cho trigger + STATE_PENDING_ORDER = 2, // da dat limit, cho khop + STATE_IN_TRADE = 3 // da khop, dang quan ly position +}; + +enum TrendDirection +{ + TREND_NONE = 0, + TREND_UP = +1, + TREND_DOWN = -1 +}; + +// Snapshot of an entry plan computed at the signal bar. +// All prices are already normalized to broker digits. +struct SignalSnapshot +{ + datetime signalBarTime; // open time of the signal bar + int direction; // +1 buy, -1 sell + double entryPrice; + double stopLossPrice; + double takeProfitPrice; + double initialRiskPrice; // |entry - SL| in price units +}; + +// Track a live pending limit order. +struct PendingContext +{ + ulong orderTicket; + int barsSincePlaced; + SignalSnapshot plan; +}; + +// Track an open position created from a filled pending order. +struct TradeContext +{ + bool isActive; + ulong positionTicket; + bool partialClosedDone; // true after 1.5R partial + BE move + SignalSnapshot plan; +}; + +#endif diff --git a/Experts/RSIForceStateEA/StateMachine.mqh b/Experts/RSIForceStateEA/StateMachine.mqh new file mode 100644 index 0000000..5d35023 --- /dev/null +++ b/Experts/RSIForceStateEA/StateMachine.mqh @@ -0,0 +1,371 @@ +#ifndef RSI_FORCE_STATE_EA__STATE_MACHINE_MQH +#define RSI_FORCE_STATE_EA__STATE_MACHINE_MQH + +// ============================================================ +// State machine: NO_TRADE -> WATCHING -> PENDING_ORDER -> IN_TRADE +// ============================================================ + +// Anti-spam guard: at most one trade per pullback. Reset when: +// - direction-trend flips (UP <-> DOWN) +// - pending order is cancelled (no actual trade happened) +// - the open trade fully closes (TP/SL/manual) +bool g_HasTradedThisPullback = false; +int g_BarsInWatching = 0; + +// Two trend trackers: +// g_LastTrend - exact value last seen (UP / DOWN / NONE), for context +// g_LastDirTrend - last DIRECTIONAL value (UP / DOWN). Used to detect a real +// directional flip even if the trend briefly went through NONE. +TrendDirection g_LastTrend = TREND_NONE; +TrendDirection g_LastDirTrend = TREND_NONE; + +// ------------------------------------------------------------ +// Misc helpers +// ------------------------------------------------------------ + +void TransitionTo(const EAState nextState) +{ + if (g_State == nextState) return; + if (InpDebugLog) + PrintFormat("[STATE] %s -> %s", EnumToString(g_State), EnumToString(nextState)); + g_State = nextState; +} + +bool IsNewBar() +{ + static datetime lastBarTime = 0; + if (g_Bars[0].time == 0) return false; + if (g_Bars[0].time == lastBarTime) return false; + lastBarTime = g_Bars[0].time; + return true; +} + +// ------------------------------------------------------------ +// Trend filter (EMA200 with optional dead-zone + flat guard) +// ------------------------------------------------------------ + +TrendDirection DetectTrend(const int signalShift) +{ + const double closePrice = g_Bars[signalShift].close; + const double ema200 = g_EMA200[signalShift]; + const double atr = g_ATR[signalShift]; + if (ema200 <= 0.0) return TREND_NONE; + + // Dead-zone around EMA200 to avoid noise. + double bufferPrice = ema200 * (InpTrendBufferPercent / 100.0); + if (InpUseATRTrendBuffer) bufferPrice = atr * InpTrendBufferATRMult; + if (bufferPrice <= 0.0) bufferPrice = 5.0 * _Point; + + // Flat EMA200 guard: skip when EMA200 barely moves between two bars. + if (InpSkipFlatEMA200) + { + const double emaDelta = MathAbs(g_EMA200[signalShift] - g_EMA200[signalShift + 1]); + const double flatThreshold = MathMax(_Point, atr * InpFlatEMA_ATRMult); + if (emaDelta <= flatThreshold) return TREND_NONE; + } + + if (closePrice > ema200 + bufferPrice) return TREND_UP; + if (closePrice < ema200 - bufferPrice) return TREND_DOWN; + return TREND_NONE; +} + +// All N RSI values inside [low, high] band -> sideway. +bool IsRSISideway(const int signalShift) +{ + if (!InpUseRSISidewayFilter) return false; + for (int i = signalShift; i < signalShift + InpSidewayLookbackBars; i++) + { + if (g_RSI[i] < InpSidewayRSILow || g_RSI[i] > InpSidewayRSIHigh) + return false; + } + return true; +} + +// ------------------------------------------------------------ +// Pullback (state NO_TRADE -> WATCHING) and trigger (WATCHING -> PENDING) +// ------------------------------------------------------------ + +bool IsPullbackInUptrend(const int signalShift) +{ + // RSI < EMA9 < WMA45 AND all three buffers slope down (current pullback). + return (g_RSI[signalShift] < g_EMA9[signalShift] + && g_EMA9[signalShift] < g_WMA45[signalShift] + && IsBufferSlopingDown(g_RSI, signalShift, InpSlopeLookbackBars) + && IsBufferSlopingDown(g_EMA9, signalShift, InpSlopeLookbackBars) + && IsBufferSlopingDown(g_WMA45, signalShift, InpSlopeLookbackBars)); +} + +bool IsPullbackInDowntrend(const int signalShift) +{ + // RSI > EMA9 > WMA45 AND all three buffers slope up. + return (g_RSI[signalShift] > g_EMA9[signalShift] + && g_EMA9[signalShift] > g_WMA45[signalShift] + && IsBufferSlopingUp(g_RSI, signalShift, InpSlopeLookbackBars) + && IsBufferSlopingUp(g_EMA9, signalShift, InpSlopeLookbackBars) + && IsBufferSlopingUp(g_WMA45, signalShift, InpSlopeLookbackBars)); +} + +// BUY trigger: RSI just crossed up WMA45 + EMA9 still below WMA45 +// + RSI didn't cross WMA45 in the previous N bars. +bool IsBuyTriggerSignal(const int signalShift) +{ + const bool rsiCrossedUp = (g_RSI[signalShift + 1] <= g_WMA45[signalShift + 1] + && g_RSI[signalShift] > g_WMA45[signalShift]); + const bool ema9StillBelow = (g_EMA9[signalShift] < g_WMA45[signalShift]); + const bool wasIsolated = !HasCrossInLastNBars(g_RSI, g_WMA45, + signalShift + 2, + InpMinBarsBetweenCrosses); + return rsiCrossedUp && ema9StillBelow && wasIsolated; +} + +// SELL trigger: mirror of BUY. +bool IsSellTriggerSignal(const int signalShift) +{ + const bool rsiCrossedDown = (g_RSI[signalShift + 1] >= g_WMA45[signalShift + 1] + && g_RSI[signalShift] < g_WMA45[signalShift]); + const bool ema9StillAbove = (g_EMA9[signalShift] > g_WMA45[signalShift]); + const bool wasIsolated = !HasCrossInLastNBars(g_RSI, g_WMA45, + signalShift + 2, + InpMinBarsBetweenCrosses); + return rsiCrossedDown && ema9StillAbove && wasIsolated; +} + +// ------------------------------------------------------------ +// Build a complete entry plan (entry / SL / TP / direction / risk) +// ------------------------------------------------------------ + +bool BuildSignalPlan(const int direction, const int signalShift, SignalSnapshot &outPlan) +{ + // Entry = midpoint between signal close and the nearest swing extreme: + // BUY -> midpoint between close and nearest swing low. + // SELL -> midpoint between close and nearest swing high. + const double swingAnchor = FindNearestSwingForEntry(direction, signalShift); + if (swingAnchor <= 0.0) return false; + + const double closePrice = g_Bars[signalShift].close; + const double entryPrice = (closePrice + swingAnchor) * 0.5; + + // Sanity: BUY limit must sit below close, SELL limit above close. + if (direction > 0 && entryPrice >= closePrice) return false; + if (direction < 0 && entryPrice <= closePrice) return false; + + const double slPrice = ComputeStopLossPrice(direction, signalShift, + entryPrice, g_ATR[signalShift]); + if (direction > 0 && slPrice >= entryPrice) return false; + if (direction < 0 && slPrice <= entryPrice) return false; + + const double riskInPrice = MathAbs(entryPrice - slPrice); + if (riskInPrice <= (2.0 * _Point)) return false; + + outPlan.signalBarTime = g_Bars[signalShift].time; + outPlan.direction = direction; + outPlan.entryPrice = NormalizePriceToTick(entryPrice); + outPlan.stopLossPrice = NormalizePriceToTick(slPrice); + outPlan.initialRiskPrice = riskInPrice; + outPlan.takeProfitPrice = NormalizePriceToTick((direction > 0) + ? (entryPrice + InpRiskRewardRatio * riskInPrice) + : (entryPrice - InpRiskRewardRatio * riskInPrice)); + return true; +} + +// ------------------------------------------------------------ +// Broker-state synchronization +// ------------------------------------------------------------ + +void ResetPullbackCycle() +{ + g_HasTradedThisPullback = false; + g_BarsInWatching = 0; +} + +// Reconcile internal state with the broker: +// PENDING_ORDER -> IN_TRADE if a position appears +// PENDING_ORDER -> NO_TRADE if order vanished without fill (cancel/reject/expire) +// IN_TRADE -> NO_TRADE if position no longer exists (TP/SL/manual) +// On both NO_TRADE transitions, also clear g_HasTradedThisPullback so the EA +// can take the next pullback opportunity in the same trend. +void SyncStateWithBroker() +{ + ulong posTicket = 0; + const bool hasPos = HasOurOpenPosition(posTicket); + const bool hasPending = HasOurPendingOrder(g_Pending.orderTicket); + + if (g_State == STATE_PENDING_ORDER) + { + if (hasPos) + { + g_OpenTrade.isActive = true; + g_OpenTrade.positionTicket = posTicket; + g_OpenTrade.plan = g_Pending.plan; + g_OpenTrade.partialClosedDone = false; + g_Pending.orderTicket = 0; + g_Pending.barsSincePlaced = 0; + TransitionTo(STATE_IN_TRADE); + return; + } + if (!hasPending) + { + // Pending was cancelled / expired / rejected externally. + g_Pending.orderTicket = 0; + g_Pending.barsSincePlaced = 0; + g_HasTradedThisPullback = false; // allow the next setup attempt + TransitionTo(STATE_NO_TRADE); + } + } + else if (g_State == STATE_IN_TRADE) + { + if (!hasPos) + { + g_OpenTrade.isActive = false; + g_OpenTrade.positionTicket = 0; + g_HasTradedThisPullback = false; // trade done -> allow next setup + TransitionTo(STATE_NO_TRADE); + } + } +} + +// ------------------------------------------------------------ +// Pending order lifecycle on every newly closed bar +// ------------------------------------------------------------ + +void TickPendingOrderLifecycle(const TrendDirection trendNow) +{ + if (g_State != STATE_PENDING_ORDER || g_Pending.orderTicket == 0) return; + + g_Pending.barsSincePlaced++; + + const bool expired = (g_Pending.barsSincePlaced >= InpPendingMaxAliveBars); + + bool invalidated = false; + if (InpInvalidateIfCrossBack) + { + if (g_Pending.plan.direction > 0) + invalidated = (trendNow != TREND_UP || g_RSI[1] < g_WMA45[1]); + else + invalidated = (trendNow != TREND_DOWN || g_RSI[1] > g_WMA45[1]); + } + + if (!expired && !invalidated) return; + + if (CancelPendingOrder(g_Pending)) + { + if (InpDebugLog) + PrintFormat("[PENDING] cancelled (%s)", expired ? "expired" : "invalidated"); + g_HasTradedThisPullback = false; // allow next pullback to retry + TransitionTo(STATE_NO_TRADE); + } +} + +// ------------------------------------------------------------ +// Per-state handlers +// ------------------------------------------------------------ + +void HandleStateNoTrade(const int signalShift, const TrendDirection trendNow) +{ + if (g_HasTradedThisPullback) return; // anti-spam: wait until reset + if (IsRSISideway(signalShift)) return; // sideway filter only blocks new setups + if (trendNow == TREND_NONE) return; // need a real direction + + if (trendNow == TREND_UP && IsPullbackInUptrend(signalShift)) + { + g_BarsInWatching = 0; + TransitionTo(STATE_WATCHING); + } + else if (trendNow == TREND_DOWN && IsPullbackInDowntrend(signalShift)) + { + g_BarsInWatching = 0; + TransitionTo(STATE_WATCHING); + } +} + +void HandleStateWatching(const int signalShift, const TrendDirection trendNow) +{ + g_BarsInWatching++; + + // Trend lost while watching -> abandon setup. + if (trendNow == TREND_NONE) + { + TransitionTo(STATE_NO_TRADE); + return; + } + + // Watching too long without a trigger -> abandon to avoid stale setups. + if (g_BarsInWatching > InpWatchingMaxBars) + { + if (InpDebugLog) Print("[WATCHING] timed out, back to NO_TRADE"); + TransitionTo(STATE_NO_TRADE); + return; + } + + // Try to trigger an entry on this bar. + SignalSnapshot plan; + ZeroMemory(plan); + bool hasSignal = false; + + if (trendNow == TREND_UP && IsBuyTriggerSignal(signalShift)) + hasSignal = BuildSignalPlan(+1, signalShift, plan); + else if (trendNow == TREND_DOWN && IsSellTriggerSignal(signalShift)) + hasSignal = BuildSignalPlan(-1, signalShift, plan); + + if (!hasSignal) return; + + if (PlaceLimitOrderFromPlan(plan, g_Pending)) + { + g_HasTradedThisPullback = true; + TransitionTo(STATE_PENDING_ORDER); + } +} + +// ------------------------------------------------------------ +// Top-level entry point: called once per closed bar +// ------------------------------------------------------------ + +void RunStateMachine() +{ + const int signalShift = InpSignalBarShift; + const TrendDirection trendNow = DetectTrend(signalShift); + + // Detect a TRUE directional flip (UP <-> DOWN). + // A short trip through TREND_NONE between two same-direction trends + // is NOT a flip and must NOT reset the cycle / abandon WATCHING. + bool dirFlipped = false; + if (trendNow != TREND_NONE) + { + if (g_LastDirTrend != TREND_NONE && g_LastDirTrend != trendNow) + dirFlipped = true; + g_LastDirTrend = trendNow; + } + + if (dirFlipped) + { + if (InpDebugLog) + PrintFormat("[TREND] direction flipped %s -> %s", + EnumToString(g_LastTrend), EnumToString(trendNow)); + ResetPullbackCycle(); + + if (g_State == STATE_PENDING_ORDER && g_Pending.orderTicket > 0) + { + if (CancelPendingOrder(g_Pending)) + { + if (InpDebugLog) Print("[PENDING] cancelled by trend flip"); + TransitionTo(STATE_NO_TRADE); + } + } + else if (g_State == STATE_WATCHING) + { + TransitionTo(STATE_NO_TRADE); + } + } + + g_LastTrend = trendNow; + + switch (g_State) + { + case STATE_NO_TRADE: HandleStateNoTrade(signalShift, trendNow); break; + case STATE_WATCHING: HandleStateWatching(signalShift, trendNow); break; + case STATE_PENDING_ORDER: TickPendingOrderLifecycle(trendNow); break; + case STATE_IN_TRADE: /* tick-level handler does the work */ break; + } +} + +#endif diff --git a/Experts/RSIForceStateEA/Trade.mqh b/Experts/RSIForceStateEA/Trade.mqh new file mode 100644 index 0000000..90400f3 --- /dev/null +++ b/Experts/RSIForceStateEA/Trade.mqh @@ -0,0 +1,328 @@ +#ifndef RSI_FORCE_STATE_EA__TRADE_MQH +#define RSI_FORCE_STATE_EA__TRADE_MQH + +#include + +// CTrade wrapper used for all order/position operations. +// Defaults (magic, deviation) are set once in InitTradeOps() at OnInit. +CTrade g_TradeOps; + +// ------------------------------------------------------------ +// Lifecycle +// ------------------------------------------------------------ + +void InitTradeOps() +{ + g_TradeOps.SetExpertMagicNumber(InpMagicNumber); + g_TradeOps.SetDeviationInPoints(10); + g_TradeOps.SetTypeFillingBySymbol(_Symbol); +} + +// ------------------------------------------------------------ +// Symbol primitives +// ------------------------------------------------------------ + +double NormalizePriceToTick(const double price) +{ + return NormalizeDouble(price, (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS)); +} + +double NormalizeVolumeToBroker(const double rawVolume) +{ + const double volMin = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN); + const double volMax = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX); + const double volStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP); + if (volStep <= 0.0) return 0.0; + double v = MathFloor(rawVolume / volStep) * volStep; + v = MathMax(v, volMin); + v = MathMin(v, volMax); + return NormalizeDouble(v, 2); +} + +double GetStopsLevelPrice() +{ + // Broker minimum distance for SL/TP/limit price from market. + const long stopsLevelPoints = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL); + return (double)stopsLevelPoints * _Point; +} + +// Position sizing from fixed % of balance and price distance entry->SL. +double CalcLotsForRisk(const double entryPrice, const double stopLossPrice) +{ + const double balance = AccountInfoDouble(ACCOUNT_BALANCE); + const double riskMoney = balance * (InpRiskPercent / 100.0); + const double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE); + const double tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE); + const double slDistance = MathAbs(entryPrice - stopLossPrice); + if (slDistance <= 0.0 || tickValue <= 0.0 || tickSize <= 0.0) return 0.0; + const double moneyPerLot = (slDistance / tickSize) * tickValue; + if (moneyPerLot <= 0.0) return 0.0; + return NormalizeVolumeToBroker(riskMoney / moneyPerLot); +} + +// ------------------------------------------------------------ +// Swing helpers (used both for entry anchor and SL anchor) +// ------------------------------------------------------------ + +// Returns the raw extreme price of the nearest swing (no buffer added). +// Used to compute the entry midpoint = (close + swingExtreme) / 2. +double FindNearestSwingForEntry(const int direction, const int signalShift) +{ + const int startShift = signalShift + 1; + const int lookback = MathMax(5, InpSwingLookbackBars); + + if (direction > 0) + { + const int swingIdx = iLowest(_Symbol, _Period, MODE_LOW, lookback, startShift); + if (swingIdx <= 0) return 0.0; + return g_Bars[swingIdx].low; + } + + const int swingIdx = iHighest(_Symbol, _Period, MODE_HIGH, lookback, startShift); + if (swingIdx <= 0) return 0.0; + return g_Bars[swingIdx].high; +} + +// Returns the SL price anchored to the nearest swing extreme + safety buffer. +double FindNearestSwingForSL(const int direction, const int signalShift) +{ + const int startShift = signalShift + 1; + const int lookback = MathMax(5, InpSwingLookbackBars); + + if (direction > 0) + { + const int swingIdx = iLowest(_Symbol, _Period, MODE_LOW, lookback, startShift); + if (swingIdx <= 0) return 0.0; + return g_Bars[swingIdx].low - (InpSL_SwingBufferPoints * _Point); + } + + const int swingIdx = iHighest(_Symbol, _Period, MODE_HIGH, lookback, startShift); + if (swingIdx <= 0) return 0.0; + return g_Bars[swingIdx].high + (InpSL_SwingBufferPoints * _Point); +} + +// Compose final SL price honoring InpStopLossMode. +double ComputeStopLossPrice(const int direction, const int signalShift, + const double entryPrice, const double atrValue) +{ + const double swingSL = FindNearestSwingForSL(direction, signalShift); + const double atrSL = (direction > 0) + ? entryPrice - (atrValue * InpSL_ATRMult) + : entryPrice + (atrValue * InpSL_ATRMult); + + if (InpStopLossMode == SL_SWING && swingSL > 0.0) return swingSL; + if (InpStopLossMode == SL_ATR) return atrSL; + if (swingSL <= 0.0) return atrSL; + + // SL_HYBRID: pick the wider (safer) stop on the correct side. + return (direction > 0) ? MathMin(swingSL, atrSL) : MathMax(swingSL, atrSL); +} + +// ------------------------------------------------------------ +// Order placement / cancellation +// ------------------------------------------------------------ + +// Validates that the limit price + SL/TP respect the broker's stops level. +// For BUY LIMIT, entry must be below current Ask by at least stopsLevel. +// For SELL LIMIT, entry must be above current Bid by at least stopsLevel. +bool ValidateLimitPrices(const SignalSnapshot &plan) +{ + const double stopsLevel = GetStopsLevelPrice(); + const double askNow = SymbolInfoDouble(_Symbol, SYMBOL_ASK); + const double bidNow = SymbolInfoDouble(_Symbol, SYMBOL_BID); + + if (plan.direction > 0) + { + if (plan.entryPrice > askNow - stopsLevel) + { + if (InpDebugLog) + PrintFormat("[ORDER] reject BUY LIMIT entry=%.5f too close to ask=%.5f (stops=%.5f)", + plan.entryPrice, askNow, stopsLevel); + return false; + } + if (plan.entryPrice - plan.stopLossPrice < stopsLevel + || plan.takeProfitPrice - plan.entryPrice < stopsLevel) + { + if (InpDebugLog) + PrintFormat("[ORDER] reject BUY LIMIT SL/TP too close to entry (stops=%.5f)", stopsLevel); + return false; + } + } + else + { + if (plan.entryPrice < bidNow + stopsLevel) + { + if (InpDebugLog) + PrintFormat("[ORDER] reject SELL LIMIT entry=%.5f too close to bid=%.5f (stops=%.5f)", + plan.entryPrice, bidNow, stopsLevel); + return false; + } + if (plan.stopLossPrice - plan.entryPrice < stopsLevel + || plan.entryPrice - plan.takeProfitPrice < stopsLevel) + { + if (InpDebugLog) + PrintFormat("[ORDER] reject SELL LIMIT SL/TP too close to entry (stops=%.5f)", stopsLevel); + return false; + } + } + return true; +} + +bool PlaceLimitOrderFromPlan(const SignalSnapshot &plan, PendingContext &pendingCtx) +{ + if (!ValidateLimitPrices(plan)) return false; + + const double lots = CalcLotsForRisk(plan.entryPrice, plan.stopLossPrice); + if (lots <= 0.0) + { + if (InpDebugLog) + PrintFormat("[ORDER] reject: lots=%.4f (risk too small or symbol info missing)", lots); + return false; + } + + const string comment = (plan.direction > 0) ? "RSIForce_BUY_LIMIT" : "RSIForce_SELL_LIMIT"; + bool placed = false; + + if (plan.direction > 0) + placed = g_TradeOps.BuyLimit(lots, + NormalizePriceToTick(plan.entryPrice), _Symbol, + NormalizePriceToTick(plan.stopLossPrice), + NormalizePriceToTick(plan.takeProfitPrice), + ORDER_TIME_GTC, 0, comment); + else + placed = g_TradeOps.SellLimit(lots, + NormalizePriceToTick(plan.entryPrice), _Symbol, + NormalizePriceToTick(plan.stopLossPrice), + NormalizePriceToTick(plan.takeProfitPrice), + ORDER_TIME_GTC, 0, comment); + + if (!placed) + { + if (InpDebugLog) + PrintFormat("[ORDER] place fail: ret=%u msg=%s", + g_TradeOps.ResultRetcode(), g_TradeOps.ResultRetcodeDescription()); + return false; + } + + pendingCtx.orderTicket = g_TradeOps.ResultOrder(); + pendingCtx.barsSincePlaced = 0; + pendingCtx.plan = plan; + + if (InpDebugLog) + PrintFormat("[ORDER] placed %s lots=%.2f entry=%.5f SL=%.5f TP=%.5f ticket=%I64u", + comment, lots, plan.entryPrice, plan.stopLossPrice, plan.takeProfitPrice, + pendingCtx.orderTicket); + return (pendingCtx.orderTicket > 0); +} + +bool CancelPendingOrder(PendingContext &pendingCtx) +{ + if (pendingCtx.orderTicket == 0) return true; + if (!g_TradeOps.OrderDelete(pendingCtx.orderTicket)) + { + if (InpDebugLog) + PrintFormat("[ORDER] delete fail: ret=%u", g_TradeOps.ResultRetcode()); + return false; + } + pendingCtx.orderTicket = 0; + pendingCtx.barsSincePlaced = 0; + return true; +} + +// ------------------------------------------------------------ +// Broker queries (filtered by symbol + magic) +// ------------------------------------------------------------ + +bool HasOurOpenPosition(ulong &outTicket) +{ + for (int i = PositionsTotal() - 1; i >= 0; i--) + { + const ulong posTicket = PositionGetTicket(i); + if (posTicket <= 0) continue; + if (PositionGetString(POSITION_SYMBOL) != _Symbol) continue; + if (PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) continue; + outTicket = posTicket; + return true; + } + return false; +} + +bool HasOurPendingOrder(const ulong ticket) +{ + if (ticket == 0) return false; + for (int i = OrdersTotal() - 1; i >= 0; i--) + { + if (OrderGetTicket(i) == ticket) return true; + } + return false; +} + +// ------------------------------------------------------------ +// Trade management: partial close at +R + move SL to BE +// ------------------------------------------------------------ + +bool ManagePartialAndBreakEven(TradeContext &openTrade) +{ + if (!openTrade.isActive || openTrade.partialClosedDone) return true; + if (!PositionSelectByTicket(openTrade.positionTicket)) return false; + if (PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) return false; + + const long posType = PositionGetInteger(POSITION_TYPE); + const double openPrice = PositionGetDouble(POSITION_PRICE_OPEN); + const double slPrice = PositionGetDouble(POSITION_SL); + const double tpPrice = PositionGetDouble(POSITION_TP); + const double posVolume = PositionGetDouble(POSITION_VOLUME); + + // Use the ACTUAL fill->SL distance as R, not the planned one + // (broker fill price may differ from planned entry price). + const double initRisk = MathAbs(openPrice - slPrice); + if (initRisk <= 0.0) return false; + + const double priceNow = (posType == POSITION_TYPE_BUY) + ? SymbolInfoDouble(_Symbol, SYMBOL_BID) + : SymbolInfoDouble(_Symbol, SYMBOL_ASK); + + const double profitDist = (posType == POSITION_TYPE_BUY) + ? (priceNow - openPrice) + : (openPrice - priceNow); + + // Not yet at the partial trigger. + if (profitDist < (InpPartialCloseAtR * initRisk)) return true; + + // Try to split off `InpPartialClosePercent`% but only if both sides + // remain >= volMin after split (otherwise just move BE). + const double volMin = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN); + const double rawClose = posVolume * (InpPartialClosePercent / 100.0); + const double closeVol = NormalizeVolumeToBroker(rawClose); + const double remainVol = NormalizeVolumeToBroker(posVolume - closeVol); + + const bool canSplit = (closeVol >= volMin) + && (remainVol >= volMin) + && (closeVol < posVolume); + if (canSplit) + { + if (!g_TradeOps.PositionClosePartial(openTrade.positionTicket, closeVol)) + { + if (InpDebugLog) + PrintFormat("[TRADE] partial-close fail: ret=%u", g_TradeOps.ResultRetcode()); + return false; + } + } + + // Always move SL to the actual fill price (BE). + if (!g_TradeOps.PositionModify(openTrade.positionTicket, + NormalizePriceToTick(openPrice), tpPrice)) + { + if (InpDebugLog) + PrintFormat("[TRADE] BE-move fail: ret=%u", g_TradeOps.ResultRetcode()); + return false; + } + + openTrade.partialClosedDone = true; + if (InpDebugLog) + PrintFormat("[TRADE] Partial=%s vol=%.2f -> SL moved to BE @ %.5f", + canSplit ? "yes" : "skipped(min vol)", closeVol, openPrice); + return true; +} + +#endif diff --git a/Experts/RSIForceStateEA/Visualizer.mqh b/Experts/RSIForceStateEA/Visualizer.mqh new file mode 100644 index 0000000..2960aa2 --- /dev/null +++ b/Experts/RSIForceStateEA/Visualizer.mqh @@ -0,0 +1,366 @@ +#ifndef RSI_FORCE_STATE_EA__VISUALIZER_MQH +#define RSI_FORCE_STATE_EA__VISUALIZER_MQH + +// ============================================================ +// Visual layer for RSIForceStateEA. +// Renders: +// - Top-left dashboard (trend, state, key indicator values). +// - Bottom-left stats panel (totals, TP, SL, net P/L). +// - TradingView-style Entry/SL/TP horizontal lines with labels. +// - Optionally attaches EMA200/RSI/EMA9/WMA45 to the chart. +// All visual objects share the prefix "RSIForce_" and are removed +// in OnDeinit via RemoveAllVisuals(). +// ============================================================ + +#define VIS_PREFIX "RSIForce_" +#define VIS_LBL_DASH_PREFIX VIS_PREFIX "DASH_" +#define VIS_LBL_STATS_PREFIX VIS_PREFIX "STATS_" +#define VIS_OBJ_LEVEL_PREFIX VIS_PREFIX "LV_" + +// Aggregated stats computed from the deal history. +struct VisualTradeStats +{ + int totalClosed; // unique closed positions (any reason) + int tpHits; // close deals with reason TP + int slHits; // close deals with reason SL + int otherCloses; // manual / expert / partial + double netPL; // sum of profit on close deals +}; + +// ------------------------------------------------------------ +// Generic object helpers +// ------------------------------------------------------------ + +void EnsureLabel(const string objName, const int xDist, const int yDist, + const ENUM_BASE_CORNER corner, const color clr, + const string text, const int fontSize = 9) +{ + if (ObjectFind(0, objName) < 0) + { + ObjectCreate(0, objName, OBJ_LABEL, 0, 0, 0); + ObjectSetInteger(0, objName, OBJPROP_CORNER, corner); + ObjectSetInteger(0, objName, OBJPROP_XDISTANCE, xDist); + ObjectSetInteger(0, objName, OBJPROP_YDISTANCE, yDist); + ObjectSetInteger(0, objName, OBJPROP_FONTSIZE, fontSize); + ObjectSetString (0, objName, OBJPROP_FONT, "Consolas"); + ObjectSetInteger(0, objName, OBJPROP_BACK, false); + ObjectSetInteger(0, objName, OBJPROP_SELECTABLE, false); + ObjectSetInteger(0, objName, OBJPROP_HIDDEN, true); + } + ObjectSetInteger(0, objName, OBJPROP_COLOR, clr); + ObjectSetString (0, objName, OBJPROP_TEXT, text); +} + +void EnsureHLine(const string objName, const double price, const color clr, + const ENUM_LINE_STYLE style, const int width, const string text) +{ + if (ObjectFind(0, objName) < 0) + { + ObjectCreate(0, objName, OBJ_HLINE, 0, 0, price); + ObjectSetInteger(0, objName, OBJPROP_BACK, true); + ObjectSetInteger(0, objName, OBJPROP_SELECTABLE, false); + ObjectSetInteger(0, objName, OBJPROP_HIDDEN, true); + } + ObjectSetDouble (0, objName, OBJPROP_PRICE, price); + ObjectSetInteger(0, objName, OBJPROP_COLOR, clr); + ObjectSetInteger(0, objName, OBJPROP_STYLE, style); + ObjectSetInteger(0, objName, OBJPROP_WIDTH, width); + ObjectSetString (0, objName, OBJPROP_TEXT, text); +} + +color TrendColor(const TrendDirection trend) +{ + if (trend == TREND_UP) return InpColorTrendUp; + if (trend == TREND_DOWN) return InpColorTrendDown; + return InpColorTrendNone; +} + +string TrendLabel(const TrendDirection trend) +{ + if (trend == TREND_UP) return "UP"; + if (trend == TREND_DOWN) return "DOWN"; + return "NONE"; +} + +// ------------------------------------------------------------ +// Top-left dashboard +// ------------------------------------------------------------ + +void DrawDashboardPanel(const TrendDirection trendNow) +{ + if (!InpShowDashboard) return; + + const int xLeft = 12; + int y = 12; + const int rowH = 18; + + EnsureLabel(VIS_LBL_DASH_PREFIX "title", xLeft, y, CORNER_LEFT_UPPER, + clrWhite, "RSIForceStateEA", 11); y += rowH; + + EnsureLabel(VIS_LBL_DASH_PREFIX "trend", xLeft, y, CORNER_LEFT_UPPER, + TrendColor(trendNow), + "Trend : " + TrendLabel(trendNow)); y += rowH; + + color stateClr = (g_State == STATE_IN_TRADE) ? clrLime + : (g_State == STATE_PENDING_ORDER) ? clrOrange + : (g_State == STATE_WATCHING) ? clrYellow + : clrSilver; + EnsureLabel(VIS_LBL_DASH_PREFIX "state", xLeft, y, CORNER_LEFT_UPPER, + stateClr, + "State : " + EnumToString(g_State)); y += rowH; + + EnsureLabel(VIS_LBL_DASH_PREFIX "rsi", xLeft, y, CORNER_LEFT_UPPER, + clrAqua, + StringFormat("RSI : %6.2f EMA9 : %6.2f WMA45: %6.2f", + g_RSI[1], g_EMA9[1], g_WMA45[1])); y += rowH; + + EnsureLabel(VIS_LBL_DASH_PREFIX "ema200", xLeft, y, CORNER_LEFT_UPPER, + InpColorEntry, + StringFormat("EMA200 : %.5f Close: %.5f", + g_EMA200[1], g_Bars[1].close)); y += rowH; + + EnsureLabel(VIS_LBL_DASH_PREFIX "atr", xLeft, y, CORNER_LEFT_UPPER, + clrSilver, + StringFormat("ATR : %.5f", g_ATR[1])); y += rowH; + + string contextLine = ""; + if (g_State == STATE_PENDING_ORDER) + contextLine = StringFormat("Pending: dir=%s entry=%.5f alive=%d/%d", + (g_Pending.plan.direction > 0 ? "BUY" : "SELL"), + g_Pending.plan.entryPrice, + g_Pending.barsSincePlaced, + InpPendingMaxAliveBars); + else if (g_State == STATE_IN_TRADE) + contextLine = StringFormat("Trade : dir=%s entry=%.5f partial=%s", + (g_OpenTrade.plan.direction > 0 ? "BUY" : "SELL"), + g_OpenTrade.plan.entryPrice, + g_OpenTrade.partialClosedDone ? "DONE" : "PEND"); + else if (g_State == STATE_WATCHING) + contextLine = StringFormat("Watch : %d/%d bars", + g_BarsInWatching, InpWatchingMaxBars); + else + contextLine = "Idle : looking for pullback..."; + + EnsureLabel(VIS_LBL_DASH_PREFIX "context", xLeft, y, CORNER_LEFT_UPPER, + clrOrange, contextLine); +} + +// ------------------------------------------------------------ +// Bottom-left stats panel +// ------------------------------------------------------------ + +bool IsDealOurs(const ulong dealTicket) +{ + if (HistoryDealGetInteger(dealTicket, DEAL_MAGIC) != InpMagicNumber) return false; + if (HistoryDealGetString (dealTicket, DEAL_SYMBOL) != _Symbol) return false; + return true; +} + +bool ContainsULong(const ulong &arr[], const int count, const ulong needle) +{ + for (int i = 0; i < count; i++) + if (arr[i] == needle) return true; + return false; +} + +void ComputeTradeStats(VisualTradeStats &stats) +{ + ZeroMemory(stats); + + const datetime fromTime = TimeCurrent() - (datetime)(InpStatsLookbackDays * 86400); + if (!HistorySelect(fromTime, TimeCurrent())) return; + + ulong seenPositions[]; + int seenCount = 0; + + const int dealsTotal = HistoryDealsTotal(); + for (int i = 0; i < dealsTotal; i++) + { + const ulong dealTicket = HistoryDealGetTicket(i); + if (dealTicket == 0) continue; + if (!IsDealOurs(dealTicket)) continue; + if (HistoryDealGetInteger(dealTicket, DEAL_ENTRY) != DEAL_ENTRY_OUT) continue; + + const long reason = HistoryDealGetInteger(dealTicket, DEAL_REASON); + const ulong positionId = (ulong)HistoryDealGetInteger(dealTicket, DEAL_POSITION_ID); + const double profit = HistoryDealGetDouble (dealTicket, DEAL_PROFIT) + + HistoryDealGetDouble (dealTicket, DEAL_SWAP) + + HistoryDealGetDouble (dealTicket, DEAL_COMMISSION); + + stats.netPL += profit; + + if (reason == DEAL_REASON_TP) stats.tpHits++; + else if (reason == DEAL_REASON_SL) stats.slHits++; + else stats.otherCloses++; + + if (positionId > 0 && !ContainsULong(seenPositions, seenCount, positionId)) + { + ArrayResize(seenPositions, seenCount + 1); + seenPositions[seenCount++] = positionId; + // Count as "fully closed" only if the position no longer exists. + if (!PositionSelectByTicket(positionId)) stats.totalClosed++; + } + } +} + +void DrawStatsPanel() +{ + if (!InpShowStatsPanel) return; + + VisualTradeStats stats; + ComputeTradeStats(stats); + + const int xLeft = 12; + const int rowH = 18; + int y = 12; + + EnsureLabel(VIS_LBL_STATS_PREFIX "pl", xLeft, y, CORNER_LEFT_LOWER, + stats.netPL >= 0 ? clrLime : clrTomato, + StringFormat("Net PL : %.2f", stats.netPL)); y += rowH; + + EnsureLabel(VIS_LBL_STATS_PREFIX "other", xLeft, y, CORNER_LEFT_LOWER, + clrSilver, + StringFormat("Other : %d", stats.otherCloses)); y += rowH; + + EnsureLabel(VIS_LBL_STATS_PREFIX "sl", xLeft, y, CORNER_LEFT_LOWER, + InpColorSL, + StringFormat("SL hit : %d", stats.slHits)); y += rowH; + + EnsureLabel(VIS_LBL_STATS_PREFIX "tp", xLeft, y, CORNER_LEFT_LOWER, + InpColorTP, + StringFormat("TP hit : %d", stats.tpHits)); y += rowH; + + EnsureLabel(VIS_LBL_STATS_PREFIX "total", xLeft, y, CORNER_LEFT_LOWER, + clrWhite, + StringFormat("Total : %d", stats.totalClosed)); y += rowH; + + EnsureLabel(VIS_LBL_STATS_PREFIX "title", xLeft, y, CORNER_LEFT_LOWER, + clrWhite, + StringFormat("-- STATS (last %dd) --", InpStatsLookbackDays), 10); +} + +// ------------------------------------------------------------ +// Entry / SL / TP horizontal lines (TradingView style) +// ------------------------------------------------------------ + +void RemoveTradeLevels() +{ + ObjectDelete(0, VIS_OBJ_LEVEL_PREFIX "entry"); + ObjectDelete(0, VIS_OBJ_LEVEL_PREFIX "sl"); + ObjectDelete(0, VIS_OBJ_LEVEL_PREFIX "tp"); +} + +void DrawTradeLevels() +{ + if (!InpShowTradeLevels) { RemoveTradeLevels(); return; } + + SignalSnapshot plan; + ZeroMemory(plan); + bool show = false; + + if (g_State == STATE_PENDING_ORDER) + { + plan = g_Pending.plan; + show = true; + } + else if (g_State == STATE_IN_TRADE) + { + plan = g_OpenTrade.plan; + show = true; + } + + if (!show) { RemoveTradeLevels(); return; } + + const string sideStr = (plan.direction > 0) ? "BUY" : "SELL"; + + EnsureHLine(VIS_OBJ_LEVEL_PREFIX "entry", plan.entryPrice, + InpColorEntry, STYLE_DOT, 1, + StringFormat("%s ENTRY %.5f", sideStr, plan.entryPrice)); + + EnsureHLine(VIS_OBJ_LEVEL_PREFIX "sl", plan.stopLossPrice, + InpColorSL, STYLE_DASH, 1, + StringFormat("SL %.5f", plan.stopLossPrice)); + + EnsureHLine(VIS_OBJ_LEVEL_PREFIX "tp", plan.takeProfitPrice, + InpColorTP, STYLE_DASH, 1, + StringFormat("TP %.5f", plan.takeProfitPrice)); +} + +// ------------------------------------------------------------ +// Indicator attach (EMA200 main + RSI cluster in subwindow) +// ------------------------------------------------------------ + +// Returns true if any indicator named like `prefix*` already lives on the +// given subwindow. Used to avoid stacking duplicates when the EA is reloaded. +bool IsIndicatorAlreadyAttached(const int subWindow, const string namePrefix) +{ + const int total = ChartIndicatorsTotal(0, subWindow); + for (int i = 0; i < total; i++) + { + const string n = ChartIndicatorName(0, subWindow, i); + if (StringFind(n, namePrefix) == 0) return true; + } + return false; +} + +void AttachIndicatorsToChart() +{ + if (!InpAttachIndicators) return; + + // EMA200 in main window. + if (!IsIndicatorAlreadyAttached(0, "Moving Average")) + { + if (!ChartIndicatorAdd(0, 0, g_hEMA200)) + PrintFormat("[VIS] add EMA200 to main fail: %d", GetLastError()); + } + + // RSI cluster in a dedicated subwindow. + // We try to find an existing subwindow that already hosts an RSI; if not, + // pass CHART_WINDOWS_TOTAL to create a fresh one. + int rsiSubWindow = -1; + const int totalWindows = (int)ChartGetInteger(0, CHART_WINDOWS_TOTAL); + for (int w = 1; w < totalWindows; w++) + { + if (IsIndicatorAlreadyAttached(w, "RSI")) + { + rsiSubWindow = w; + break; + } + } + if (rsiSubWindow < 0) rsiSubWindow = totalWindows; // create new + + if (!IsIndicatorAlreadyAttached(rsiSubWindow, "RSI")) + { + if (!ChartIndicatorAdd(0, rsiSubWindow, g_hRSI)) + PrintFormat("[VIS] add RSI to subwindow fail: %d", GetLastError()); + } + if (!IsIndicatorAlreadyAttached(rsiSubWindow, "Moving Average")) + { + if (!ChartIndicatorAdd(0, rsiSubWindow, g_hEMA9)) + PrintFormat("[VIS] add RSI_EMA9 to subwindow fail: %d", GetLastError()); + if (!ChartIndicatorAdd(0, rsiSubWindow, g_hWMA45)) + PrintFormat("[VIS] add RSI_WMA45 to subwindow fail: %d", GetLastError()); + } + + ChartRedraw(0); +} + +// ------------------------------------------------------------ +// Top-level orchestration +// ------------------------------------------------------------ + +void DrawAllVisuals(const TrendDirection trendNow) +{ + if (!InpVisualize) return; + DrawDashboardPanel(trendNow); + DrawStatsPanel(); + DrawTradeLevels(); +} + +void RemoveAllVisuals() +{ + ObjectsDeleteAll(0, VIS_PREFIX); + ChartRedraw(0); +} + +#endif diff --git a/Experts/SimpleFVG/Config.mqh b/Experts/SimpleFVG/Config.mqh index 85d7e5b..f4db4b8 100644 --- a/Experts/SimpleFVG/Config.mqh +++ b/Experts/SimpleFVG/Config.mqh @@ -14,21 +14,30 @@ input int InpEMAFastPeriod = 34; input int InpEMASlowPeriod = 89; //--- Step 2: FVG Detection --- -input int InpFVGLookbackBars = 100; -input int InpFVGMaxAgeBars = 50; -input double InpFVGMinBodyPct = 50.0; -input double InpFVGMinSizePoints = 0; -input double InpFVGTouchedPercent = 33.0; -input double InpFVGMinGapVsImpulsePct = 30.0; +input int InpFVGLookbackBars = 120; +input int InpFVGMaxAgeBars = 36; +input double InpFVGMinBodyPct = 55.0; +input double InpFVGMinSizePoints = 120; +input double InpFVGTouchedPercent = 35.0; +input double InpFVGMinGapVsImpulsePct = 35.0; input double InpFVGMaxOuterBarRatio = 2.0; //--- Step 3: Trading --- input bool InpTradeEnabled = true; -input double InpRiskPercentPerR = 1.0; +input double InpRiskPercentPerR = 1; input double InpRRRatio = 2.2; -input int InpMaxLimitOrders = 3; -input int InpLimitMaxAgeBars = 24; +input int InpMaxLimitOrders = 1; +input int InpLimitMaxAgeBars = 12; input long InpEAMagic = 123456; +input int InpMaxSpreadPoints = 350; // XAUUSD-friendly default +input bool InpUseSessionFilter = true; +input int InpSessionStartHour = 7; // server time hour [0..23] +input int InpSessionEndHour = 23; // server time hour [0..23], supports overnight window +input bool InpUseATRFilter = true; +input int InpATRPeriod = 14; +input double InpMinATRPoints = 1200; // skip low-volatility regime +input double InpMaxATRPoints = 7000; // skip extreme-volatility regime +input double InpLowTFEntryRangeBufferPoints = 60; // HTF-LTF mapping tolerance //--- Step 4: Drawing --- input color InpColorBullFVG = C'30,80,140'; diff --git a/Experts/SimpleFVG/Drawing.mqh b/Experts/SimpleFVG/Drawing.mqh index cdb5a84..ab89cb4 100644 --- a/Experts/SimpleFVG/Drawing.mqh +++ b/Experts/SimpleFVG/Drawing.mqh @@ -228,6 +228,8 @@ void DrawFVGZones() stateStr = " [TOUCHED]"; else if(IsZoneMitigated(g_FVGZones[i])) stateStr = " [MITIGATED]"; + if(g_FVGZones[i].tradeLocked) + stateStr += " [LOCKED]"; color labelColor; if(IsZoneMitigated(g_FVGZones[i])) diff --git a/Experts/SimpleFVG/FVG.mqh b/Experts/SimpleFVG/FVG.mqh index 59657c2..f88c1f4 100644 --- a/Experts/SimpleFVG/FVG.mqh +++ b/Experts/SimpleFVG/FVG.mqh @@ -29,6 +29,9 @@ struct FVGZone double slReferencePrice; datetime createdTime; int ageInBars; + bool tradeLocked; // true after one order attempt is placed from this zone + ulong linkedOrderTicket; + datetime tradeLockedTime; }; //+------------------------------------------------------------------+ @@ -302,6 +305,103 @@ bool GetLatestLowTFFVG(string symbol, return false; } +//+------------------------------------------------------------------+ +//| Low-TF FVG nearest and aligned with HTF zone price range | +//+------------------------------------------------------------------+ +bool GetLatestLowTFFVGInRange(string symbol, + ENUM_TIMEFRAMES tf, + ENUM_FVG_TYPE type, + int maxLookbackBars, + double rangeLower, + double rangeUpper, + double rangeBufferPoints, + double &outUpper, + double &outLower, + double &outBarALow, + double &outBarAHigh) +{ + int totalBars = Bars(symbol, tf); + int maxShift = MathMin(maxLookbackBars, totalBars - 3); + if(maxShift < 1) + return false; + + double maxOuterRatio = InpFVGMaxOuterBarRatio; + double minGapVsBody = InpFVGMinGapVsImpulsePct / 100.0; + double bufferPrice = rangeBufferPoints * _Point; + double minAllowed = rangeLower - bufferPrice; + double maxAllowed = rangeUpper + bufferPrice; + + for(int shift = 1; shift <= maxShift; shift++) + { + int shiftA = shift + 2; + int shiftB = shift + 1; + int shiftC = shift; + + double candleA_High = iHigh(symbol, tf, shiftA); + double candleA_Low = iLow (symbol, tf, shiftA); + double candleB_High = iHigh(symbol, tf, shiftB); + double candleB_Low = iLow (symbol, tf, shiftB); + double candleB_Open = iOpen (symbol, tf, shiftB); + double candleB_Close = iClose(symbol, tf, shiftB); + double candleC_High = iHigh(symbol, tf, shiftC); + double candleC_Low = iLow (symbol, tf, shiftC); + + double rangeA = candleA_High - candleA_Low; + double rangeB = candleB_High - candleB_Low; + double rangeC = candleC_High - candleC_Low; + double bodyB = MathAbs(candleB_Close - candleB_Open); + + if(rangeB <= 0 || bodyB <= 0) + continue; + if(rangeA > maxOuterRatio * rangeB || rangeC > maxOuterRatio * rangeB) + continue; + if(!IsImpulseCandleStrong(symbol, tf, shiftB)) + continue; + + if(type == FVG_BULLISH) + { + if(candleA_High >= candleC_Low || candleB_Close <= candleB_Open) + continue; + + double gap = candleC_Low - candleA_High; + double gapRatio = gap / bodyB; + if(gapRatio < minGapVsBody) + continue; + + double entry = candleC_Low; + if(entry < minAllowed || entry > maxAllowed) + continue; + + outUpper = candleC_Low; + outLower = candleA_High; + outBarALow = candleA_Low; + outBarAHigh = candleA_High; + return true; + } + else + { + if(candleA_Low <= candleC_High || candleB_Close >= candleB_Open) + continue; + + double gap = candleA_Low - candleC_High; + double gapRatio = gap / bodyB; + if(gapRatio < minGapVsBody) + continue; + + double entry = candleC_High; + if(entry < minAllowed || entry > maxAllowed) + continue; + + outUpper = candleA_Low; + outLower = candleC_High; + outBarALow = candleA_Low; + outBarAHigh = candleA_High; + return true; + } + } + return false; +} + //+------------------------------------------------------------------+ //| Check if price has mitigated (filled through) any active FVG | //+------------------------------------------------------------------+ diff --git a/Experts/SimpleFVG/Trade.mqh b/Experts/SimpleFVG/Trade.mqh index e207f3e..465e1d1 100644 --- a/Experts/SimpleFVG/Trade.mqh +++ b/Experts/SimpleFVG/Trade.mqh @@ -13,6 +13,135 @@ //+------------------------------------------------------------------+ //| Internal helpers | //+------------------------------------------------------------------+ +string FVGTypeToString(ENUM_FVG_TYPE type) +{ + return (type == FVG_BULLISH) ? "BULL" : "BEAR"; +} + +void LogDecisionTrace(const string stage, const string message) +{ + if(!InpDebugLog) + return; + PrintFormat("[TRACE][%s] %s", stage, message); +} + +bool IsSpreadAcceptable(string symbol) +{ + if(InpMaxSpreadPoints <= 0) + return true; + + double ask = SymbolInfoDouble(symbol, SYMBOL_ASK); + double bid = SymbolInfoDouble(symbol, SYMBOL_BID); + double spreadPoints = -1.0; + if(ask > 0.0 && bid > 0.0) + spreadPoints = (ask - bid) / _Point; + else + { + // In Strategy Tester, BID/ASK can be unavailable on some bars. + // Fallback to broker/tester spread setting (already in points). + long spreadInt = SymbolInfoInteger(symbol, SYMBOL_SPREAD); + if(spreadInt > 0) + spreadPoints = (double)spreadInt; + } + + if(spreadPoints < 0.0) + return false; + + return (spreadPoints <= InpMaxSpreadPoints); +} + +double GetCurrentSpreadPoints(string symbol) +{ + double ask = SymbolInfoDouble(symbol, SYMBOL_ASK); + double bid = SymbolInfoDouble(symbol, SYMBOL_BID); + if(ask > 0.0 && bid > 0.0) + return (ask - bid) / _Point; + + long spreadInt = SymbolInfoInteger(symbol, SYMBOL_SPREAD); + if(spreadInt > 0) + return (double)spreadInt; + + return -1.0; +} + +bool IsTradingSessionOpen() +{ + if(!InpUseSessionFilter) + return true; + + datetime now = TimeCurrent(); + if(now == 0) + return false; + + MqlDateTime dt; + TimeToStruct(now, dt); + int hour = dt.hour; + + int startHour = MathMax(0, MathMin(23, InpSessionStartHour)); + int endHour = MathMax(0, MathMin(23, InpSessionEndHour)); + + if(startHour == endHour) + return true; + if(startHour < endHour) + return (hour >= startHour && hour < endHour); + return (hour >= startHour || hour < endHour); // overnight session +} + +int GetCurrentServerHour() +{ + datetime now = TimeCurrent(); + if(now == 0) + return -1; + + MqlDateTime dt; + TimeToStruct(now, dt); + return dt.hour; +} + +void GetNormalizedSessionHours(int &outStartHour, int &outEndHour) +{ + outStartHour = MathMax(0, MathMin(23, InpSessionStartHour)); + outEndHour = MathMax(0, MathMin(23, InpSessionEndHour)); +} + +double GetATRPoints(string symbol, ENUM_TIMEFRAMES tf, int period) +{ + if(period <= 1) + return -1.0; + + int handle = iATR(symbol, tf, period); + if(handle == INVALID_HANDLE) + return -1.0; + + double buffer[1]; + int copied = CopyBuffer(handle, 0, 1, 1, buffer); + IndicatorRelease(handle); + if(copied < 1 || buffer[0] <= 0.0) + return -1.0; + + return buffer[0] / _Point; +} + +bool IsATRRegimeValid(string symbol) +{ + if(!InpUseATRFilter) + return true; + + double atrPoints = GetATRPoints(symbol, InpTimeframe, InpATRPeriod); + if(atrPoints <= 0.0) + return false; + if(InpMinATRPoints > 0.0 && atrPoints < InpMinATRPoints) + return false; + if(InpMaxATRPoints > 0.0 && atrPoints > InpMaxATRPoints) + return false; + return true; +} + +double GetCurrentATRPoints(string symbol) +{ + return GetATRPoints(symbol, InpTimeframe, InpATRPeriod); +} + int CountOurPositions() { int count = 0; @@ -382,9 +511,35 @@ void ManageFVGTrades() if(currentLimits >= InpMaxLimitOrders) return; + string symbol = GetTradeSymbol(); + if(!IsSpreadAcceptable(symbol)) + { + double spreadPts = GetCurrentSpreadPoints(symbol); + LogDecisionTrace("FILTER", StringFormat("Skip entries: spread %.1f > max %d points", + spreadPts, InpMaxSpreadPoints)); + return; + } + if(!IsTradingSessionOpen()) + { + int startHour, endHour; + GetNormalizedSessionHours(startHour, endHour); + int currentHour = GetCurrentServerHour(); + LogDecisionTrace("FILTER", StringFormat("Skip entries: session closed (hour=%d, window=%02d-%02d, useSession=%s)", + currentHour, startHour, endHour, + InpUseSessionFilter ? "true" : "false")); + return; + } + if(!IsATRRegimeValid(symbol)) + { + double atrPoints = GetCurrentATRPoints(symbol); + LogDecisionTrace("FILTER", StringFormat("Skip entries: ATR %.1f outside [%.1f..%.1f] points (period=%d, useATR=%s)", + atrPoints, InpMinATRPoints, InpMaxATRPoints, + InpATRPeriod, InpUseATRFilter ? "true" : "false")); + return; + } + // Chỉ tìm tín hiệu low TF FVG khi high TF FVG đã TOUCHED (giá lấp đủ %), không trigger khi mới chạm cạnh ENUM_TREND_DIRECTION trend = g_CurrentTrend; - string symbol = GetTradeSymbol(); ENUM_TIMEFRAMES lowTF = GetConfirmationTimeframe(InpTimeframe); const int LOW_TF_FVG_LOOKBACK = 15; @@ -392,25 +547,52 @@ void ManageFVGTrades() { FVGZone zone = g_FVGZones[i]; if(!IsZoneActive(zone) || IsZoneMitigated(zone)) + { + LogDecisionTrace("ZONE", StringFormat("#%d skip: inactive/mitigated", i)); continue; + } + + if(zone.tradeLocked) + { + LogDecisionTrace("ZONE", StringFormat("#%d %s skip: locked", i, FVGTypeToString(zone.type))); + continue; + } if(zone.type == FVG_BULLISH && trend != TREND_BULLISH) + { + LogDecisionTrace("ZONE", StringFormat("#%d BULL blocked by EMA trend", i)); continue; + } if(zone.type == FVG_BEARISH && trend != TREND_BEARISH) + { + LogDecisionTrace("ZONE", StringFormat("#%d BEAR blocked by EMA trend", i)); continue; + } // Điều kiện vào lệnh: FVG high TF phải đã TOUCHED (giá lấp >= InpFVGTouchedPercent), không chỉ chạm cạnh if(!IsZoneTouched(zone)) + { + LogDecisionTrace("ZONE", StringFormat("#%d %s not touched", i, FVGTypeToString(zone.type))); continue; + } // Chỉ đặt lệnh khi có low TF để xác nhận (H1->M5, H4->M15, M15->M2) if(lowTF == InpTimeframe) + { + LogDecisionTrace("ZONE", StringFormat("#%d skip: lowTF mapping unavailable", i)); continue; + } double ltfUpper, ltfLower, ltfBarALow, ltfBarAHigh; - if(!GetLatestLowTFFVG(symbol, lowTF, zone.type, LOW_TF_FVG_LOOKBACK, - ltfUpper, ltfLower, ltfBarALow, ltfBarAHigh)) + if(!GetLatestLowTFFVGInRange(symbol, lowTF, zone.type, LOW_TF_FVG_LOOKBACK, + zone.lowerEdge, zone.upperEdge, + InpLowTFEntryRangeBufferPoints, + ltfUpper, ltfLower, ltfBarALow, ltfBarAHigh)) + { + LogDecisionTrace("ZONE", StringFormat("#%d %s no LTF FVG aligned to HTF zone [%.5f..%.5f]", + i, FVGTypeToString(zone.type), zone.lowerEdge, zone.upperEdge)); continue; + } // Entry theo low TF FVG; SL = bar B của high TF FVG double entryPrice, slPrice; @@ -427,10 +609,19 @@ void ManageFVGTrades() if(PlaceLimitFromLowTF(symbol, zone.type, entryPrice, slPrice)) { - // Mỗi FVG chỉ được dùng để trade 1 lần - g_FVGZones[i].status = EXPIRED; + // v2 state: lock zone after first successful placement; keep visible for diagnostics. + g_FVGZones[i].tradeLocked = true; + g_FVGZones[i].tradeLockedTime = TimeCurrent(); + g_FVGZones[i].linkedOrderTicket = 0; + LogDecisionTrace("ORDER", StringFormat("#%d %s placed @%.5f SL=%.5f HTF[%.5f..%.5f] LTF[%.5f..%.5f]", + i, FVGTypeToString(zone.type), entryPrice, slPrice, + zone.lowerEdge, zone.upperEdge, ltfLower, ltfUpper)); currentLimits++; } + else + { + LogDecisionTrace("ORDER", StringFormat("#%d %s failed @%.5f", i, FVGTypeToString(zone.type), entryPrice)); + } } } diff --git a/Include/Files/FilePipe.mqh b/Include/Files/FilePipe.mqh index 1afe94c..6feb430 100644 --- a/Include/Files/FilePipe.mqh +++ b/Include/Files/FilePipe.mqh @@ -204,7 +204,7 @@ bool CFilePipe::ReadInteger(T &value) if(WaitForRead(sizeof(T))) { ResetLastError(); - value=FileReadInteger(m_handle,sizeof(T)); + value=(T)FileReadInteger(m_handle,sizeof(T)); return(GetLastError()==0); } //--- failure diff --git a/Include/Graphics/Graphic.mqh b/Include/Graphics/Graphic.mqh index 0b87568..f594796 100644 Binary files a/Include/Graphics/Graphic.mqh and b/Include/Graphics/Graphic.mqh differ diff --git a/experts.dat b/experts.dat index bd4b195..9ba8826 100644 Binary files a/experts.dat and b/experts.dat differ