diff --git a/Experts/RSIForceStateEA.mq5 b/Experts/RSIForceStateEA.mq5 deleted file mode 100644 index 0f6d157..0000000 --- a/Experts/RSIForceStateEA.mq5 +++ /dev/null @@ -1,159 +0,0 @@ -//+------------------------------------------------------------------+ -//| 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 deleted file mode 100644 index ffdfe20..0000000 --- a/Experts/RSIForceStateEA/Config.mqh +++ /dev/null @@ -1,74 +0,0 @@ -#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 deleted file mode 100644 index a7bd26d..0000000 --- a/Experts/RSIForceStateEA/Indicators.mqh +++ /dev/null @@ -1,122 +0,0 @@ -#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 deleted file mode 100644 index 7f80518..0000000 --- a/Experts/RSIForceStateEA/README.md +++ /dev/null @@ -1,456 +0,0 @@ -# RSIForceStateEA - -EA giao dịch pullback theo động lực RSI, lọc xu hướng bằng EMA200, điều phối bằng state -machine 4 trạng thái. Mỗi pullback chỉ một lệnh, vào bằng **BUY/SELL LIMIT** (không đuổi giá market). - -## 1) Cấu trúc thư mục - -- EA chính: `Experts/RSIForceStateEA.mq5` -- Các module: - - `Experts/RSIForceStateEA/Config.mqh` — toàn bộ tham số đầu vào (`input`) - - `Experts/RSIForceStateEA/State.mqh` — `enum` + `struct` (không chứa biến global) - - `Experts/RSIForceStateEA/Indicators.mqh` — handle chỉ báo + buffer + hàm slope/cross - - `Experts/RSIForceStateEA/Trade.mqh` — đặt lệnh, khối lượng theo rủi ro, partial + BE - - `Experts/RSIForceStateEA/StateMachine.mqh` — luồng 4 trạng thái - - `Experts/RSIForceStateEA/Visualizer.mqh` — dashboard, bảng thống kê, mức Entry/SL/TP - - `Experts/RSIForceStateEA/README.md` — tài liệu này - -Thứ tự `#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 tổng thể - -### 2.1 Các chỉ báo - -- `RSI(InpRSIPeriod)` — động lực giá. -- `EMA(InpRSI_EMA9Period)` — tính **trên** chuỗi RSI. -- `WMA(InpRSI_WMA45Period)` — tính **trên** chuỗi RSI. -- `EMA(InpEMA200Period)` — trên **giá đóng** — lọc xu hướng. -- `ATR(InpATRPeriod)` — dùng cho vùng đệm trend và SL. - -### 2.2 Bộ lọc xu hướng (trend) - -- **Uptrend** khi `Close > EMA200 + buffer`. -- **Downtrend** khi `Close < EMA200 - buffer`. -- **Buffer**: - - Theo `%` EMA200 mặc định (`InpTrendBufferPercent = 0.10%`), hoặc - - Theo ATR nếu `InpUseATRTrendBuffer = true`. -- `InpSkipFlatEMA200`: bỏ qua khi EMA200 “phẳng” (chênh lệch giữa hai nến ≤ `ATR * InpFlatEMA_ATRMult`). - -### 2.3 Bốn trạng thái - -| Trạng thái | Ý nghĩa | -| ----------- | ------- | -| `STATE_NO_TRADE` | Chưa có setup hợp lệ | -| `STATE_WATCHING` | Đã có pullback hợp lệ, đang chờ tín hiệu vào lệnh | -| `STATE_PENDING_ORDER` | Đã đặt BUY/SELL LIMIT, chờ khớp | -| `STATE_IN_TRADE` | Đã khớp, đang quản lý vị thế (partial + BE + TP) | - -### 2.4 Pullback (`NO_TRADE` → `WATCHING`) - -- Pullback trong **uptrend**: - - `RSI < EMA9 < WMA45` - - Cả ba đường cùng **slope giảm** (kiểm tra trên `InpSlopeLookbackBars` nến). -- Pullback trong **downtrend**: đối xứng (slope tăng). - -### 2.5 Kích hoạt vào lệnh (`WATCHING` → `PENDING_ORDER`) - -- **Tín hiệu BUY**: - - RSI cắt **lên** WMA45 (so sánh nến `InpSignalBarShift` với nến trước). - - EMA9 vẫn **dưới** WMA45. - - Trong `InpMinBarsBetweenCrosses` nến trước đó **không** có cắt RSI/WMA45 (tín hiệu “cô lập”, tránh nhiễu sát WMA45). -- **Tín hiệu SELL**: đối xứng. - -### 2.6 Đặt lệnh LIMIT - -- `Entry = (Close(nến tín hiệu) + SwingExtreme) / 2` - - **BUY**: SwingExtreme = đáy gần nhất trong `InpSwingLookbackBars`. - - **SELL**: SwingExtreme = đỉnh gần nhất. -- Lệnh: `BuyLimit` / `SellLimit` (không vào market). -- Pending tối đa `InpPendingMaxAliveBars` nến; hết hạn → hủy. -- Nếu `InpInvalidateIfCrossBack = true`: RSI cắt ngược lại WMA45 hoặc mất trend → hủy. -- `WATCHING` tối đa `InpWatchingMaxBars` nến không có trigger → về `NO_TRADE`. - -### 2.7 Stop Loss - -| Chế độ | Công thức | -| ------ | --------- | -| `SL_SWING` | Cực trị swing ± `InpSL_SwingBufferPoints` (point) | -| `SL_ATR` | `Entry ± ATR * InpSL_ATRMult` | -| `SL_HYBRID` | Chọn SL **rộng hơn** giữa swing và ATR (mặc định, an toàn hơn) | - -### 2.8 Take Profit - -- `TP = Entry ± R * InpRiskRewardRatio` (mặc định `2R`). - -### 2.9 Quản lý khi `IN_TRADE` - -- Rủi ro: `InpRiskPercent` (% số dư, mặc định 1%). -- Khi giá đạt `InpPartialCloseAtR` lần **R thực tế** (mặc định `1.5R`): - - Đóng `InpPartialClosePercent` % khối lượng (mặc định 50%). - - Dời SL về **giá khớp** (BE). -- Nếu không chia được lot (bị `volume min` chặn): vẫn dời SL về BE, bỏ qua partial. -- Không có hành động đặc biệt tại `1R`. - -### 2.10 Chống spam (một pullback — một lệnh thử) - -- Biến `g_HasTradedThisPullback`: sau khi **đặt thành công** pending cho pullback hiện tại → bật. -- **Reset** `g_HasTradedThisPullback` khi: - - Pending bị hủy / hết hạn / invalid (chưa khớp → được thử pullback khác). - - Vị thế đã đóng hoàn toàn (TP/SL/đóng tay). - - **Đảo chiều xu hướng có hướng** UP ↔ DOWN (đi qua `TREND_NONE` ngắn **không** coi là đảo chiều). -- Khi đảo chiều UP ↔ DOWN mà còn pending ngược hướng → hủy pending. - -### 2.11 Bộ lọc RSI sideway - -- `InpUseRSISidewayFilter`: nếu **tất cả** `InpSidewayLookbackBars` giá trị RSI gần nhất nằm trong `[InpSidewayRSILow, InpSidewayRSIHigh]` → bỏ qua tìm setup mới. -- Bộ lọc này **chỉ** chặn nhánh `NO_TRADE → WATCHING`; **không** chặn vòng đời pending và **không** chặn quản lý vị thế. - -## 3) Cách biên dịch và chạy - -1. Mở MetaEditor. -2. Mở `MQL5/Experts/RSIForceStateEA.mq5` và biên dịch (F7). -3. Quay lại MT5, gắn EA vào biểu đồ cần giao dịch. -4. Bật **Algo Trading**. -5. Có thể chỉnh `Inputs` ngay trên giao diện khi gắn EA. - -## 4) Các tham số quan trọng (mặc định) - -| Tham số | Mặc định | Mô tả | -| ------- | -------- | ----- | -| `InpMagicNumber` | 26050901 | Magic; đổi nếu chạy nhiều bản EA | -| `InpRiskPercent` | 1.0 | % số dư / mỗi lệnh | -| `InpRiskRewardRatio` | 2.0 | TP = R × hệ số này | -| `InpPartialCloseAtR` | 1.5 | Chốt một phần khi đạt R này (phải < RR) | -| `InpPartialClosePercent` | 50.0 | % khối lượng chốt partial | -| `InpPendingMaxAliveBars` | 5 | Hủy pending sau N nến | -| `InpWatchingMaxBars` | 10 | Thoát WATCHING nếu không có trigger | -| `InpMinBarsBetweenCrosses` | 10 | Khoảng cách tối thiểu giữa các lần cắt RSI/WMA45 | -| `InpSlopeLookbackBars` | 3 | Số nến kiểm tra slope | -| `InpSwingLookbackBars` | 20 | Cửa sổ tìm swing cho entry/SL | -| `InpStopLossMode` | SL_HYBRID | `SL_SWING` / `SL_ATR` / `SL_HYBRID` | -| `InpSL_ATRMult` | 1.2 | Hệ số ATR cho SL kiểu ATR | -| `InpSL_SwingBufferPoints` | 20 | Đệm thêm (point) ngoài cực trị swing | -| `InpUseRSISidewayFilter` | true | Bật/tắt lọc sideway RSI | -| `InpSidewayRSILow / High` | 45 / 55 | Dải sideway theo RSI | -| `InpUseATRTrendBuffer` | false | Dùng buffer trend theo ATR | - -## 5) Gợi ý kiểm thử và tối ưu - -- Nên backtest từng symbol ít nhất 6–12 tháng trước khi chạy thật. -- Thị trường biến động mạnh: thử `InpUseATRTrendBuffer = true`. -- Muốn **ít tín hiệu hơn nhưng chọn lọc hơn**: - - Tăng `InpMinBarsBetweenCrosses` - - Tăng `InpSlopeLookbackBars` - - Giữ `SL_HYBRID` -- Muốn **nhiều tín hiệu hơn**: - - Giảm `InpSlopeLookbackBars` xuống 2 - - Tắt `InpUseRSISidewayFilter` -- Theo dõi tab **Experts** / **Journal** để xem log `[STATE]`, `[TREND]`, `[PENDING]`, `[TRADE]`. - -## 6) Hiển thị trực quan (Visualization) - -EA có thể tự vẽ để quan sát và đánh giá (bật/tắt bằng `InpVisualize` và các cờ con). - -### 6.1 Dashboard (góc trên-trái) - -Hiển thị: - -- **Trend**: UP / DOWN / NONE (màu xanh / đỏ / xám) -- **State**: `STATE_NO_TRADE` / `STATE_WATCHING` / `STATE_PENDING_ORDER` / `STATE_IN_TRADE` -- **RSI**: giá trị RSI, EMA9, WMA45 tại nến tín hiệu (`InpSignalBarShift`) -- **EMA200**: giá trị EMA200 + giá đóng hiện tại -- **ATR**: giá trị ATR -- **Context**: tùy trạng thái — ví dụ số nến WATCHING, thời gian sống pending, đã partial hay chưa - -### 6.2 Bảng thống kê (góc dưới-trái) - -Quét lịch sử `InpStatsLookbackDays` ngày (mặc định 60), lọc theo `InpMagicNumber` + `_Symbol`: - -- **Total**: số vị thế đã đóng (gộp theo `POSITION_ID`, tránh đếm trùng partial) -- **TP hit**: số deal đóng với lý do `DEAL_REASON_TP` -- **SL hit**: số deal đóng với lý do `DEAL_REASON_SL` -- **Other**: đóng tay / partial / đóng bởi expert -- **Net PL**: tổng P/L (profit + swap + hoa hồng) - -### 6.3 Mức giá Entry / SL / TP (kiểu TradingView) - -Khi `STATE_PENDING_ORDER` hoặc `STATE_IN_TRADE`, vẽ 3 đường ngang: - -- **ENTRY** (chấm, màu `InpColorEntry`) -- **SL** (gạch, màu `InpColorSL`) -- **TP** (gạch, màu `InpColorTP`) - -Tự xóa khi về `NO_TRADE`. - -### 6.4 Tự gắn chỉ báo lên chart - -Khi `InpAttachIndicators = true` (mặc định): - -- **EMA200** vào cửa sổ chính. -- **RSI**, **EMA trên RSI**, **WMA trên RSI** vào **cửa sổ phụ** (subwindow). - -Có thể tắt từng phần: - -| Tham số | Mặc định | Tác dụng | -| ------- | -------- | -------- | -| `InpVisualize` | true | Bật/tắt toàn bộ lớp hiển thị | -| `InpAttachIndicators` | true | Tự thêm EMA200 + cụm RSI lên chart | -| `InpShowDashboard` | true | Panel góc trên-trái | -| `InpShowStatsPanel` | true | Panel góc dưới-trái | -| `InpShowTradeLevels` | true | Đường Entry/SL/TP | -| `InpStatsLookbackDays` | 60 | Số ngày quét thống kê | - -Mọi đối tượng đồ họa dùng tiền tố `RSIForce_` và được xóa trong `OnDeinit`. - -## 7) Luồng chạy chi tiết (từng bước) - -### 7.1 Toàn cảnh kiến trúc - -``` -+-------------------------------------------------------------+ -| RSIForceStateEA.mq5 | -| (điểm vào — chỉ có OnInit / OnTick / OnTradeTransaction | -| / OnDeinit, không chứa logic nghiệp vụ) | -+-------------------------------------------------------------+ - | - | #include theo thứ tự: - v -+-------- Tầng 1 (dữ liệu + kiểu) ---------------------------+ -| Config.mqh — toàn bộ input | -| State.mqh — enum EAState, struct SignalSnapshot... | -| Indicators.mqh — handle iRSI/iMA + buffer g_RSI[]... | -+------------------------------------------------------------+ - | - | Khai báo global trong .mq5: - | g_State, g_Pending, g_OpenTrade - v -+-------- Tầng 2 (logic) ------------------------------------+ -| Trade.mqh — CalcLotsForRisk, PlaceLimitOrderFromPlan,| -| ManagePartialAndBreakEven... | -| StateMachine.mqh — DetectTrend, IsPullback*, IsBuyTrigger*, | -| BuildSignalPlan, RunStateMachine... | -+------------------------------------------------------------+ - | - v -+-------- Tầng 3 (giao diện) --------------------------------+ -| Visualizer.mqh — DrawDashboardPanel, DrawStatsPanel, | -| DrawTradeLevels, AttachIndicatorsToChart | -+------------------------------------------------------------+ -``` - -### 7.2 Vòng đời — từ lúc gắn EA - -#### Bước 0. MT5 gọi `OnInit()` (file `RSIForceStateEA.mq5`) - -``` -RSIForceStateEA.mq5 :: OnInit() - | - +-- ValidateInputs() (file .mq5 — kiểm tra cấu hình) - | nếu sai -> INIT_PARAMETERS_INCORRECT (EA không khởi động) - | - +-- ZeroMemory(g_Pending), ZeroMemory(g_OpenTrade) - | đưa global về trạng thái sạch - | - +-- InitIndicators() (Indicators.mqh) - | | - | +-- iRSI(...) -> g_hRSI - | +-- iMA(g_hRSI, EMA) -> g_hEMA9 (EMA trên RSI) - | +-- iMA(g_hRSI, WMA) -> g_hWMA45 (WMA trên RSI) - | +-- iMA(close, EMA) -> g_hEMA200 (EMA trên giá đóng) - | +-- iATR(...) -> g_hATR - | +-- ArraySetAsSeries(...) cho mọi buffer - | - +-- RefreshIndicatorData() (Indicators.mqh) - | | - | +-- CopyRates() -> g_Bars[] - | +-- CopyBuffer() -> g_RSI[], g_EMA9[], g_WMA45[], g_EMA200[], g_ATR[] - | nếu chart chưa đủ lịch sử (< 200 nến) -> INIT_FAILED - | - +-- InitTradeOps() (Trade.mqh) - | | - | +-- g_TradeOps.SetExpertMagicNumber(InpMagicNumber) - | +-- g_TradeOps.SetDeviationInPoints(10) - | +-- g_TradeOps.SetTypeFillingBySymbol(_Symbol) - | - +-- g_LastTrend = DetectTrend(InpSignalBarShift) - | khởi tạo trend ngay từ đầu để tick đầu không báo “đảo chiều” giả - | - +-- AttachIndicatorsToChart() (Visualizer.mqh) - | | - | +-- IsIndicatorAlreadyAttached(...) — tránh trùng khi reload EA - | +-- ChartIndicatorAdd(0, 0, g_hEMA200) -> cửa sổ chính - | +-- ChartIndicatorAdd(0, sub, g_hRSI) -> subwindow - | +-- ChartIndicatorAdd(0, sub, g_hEMA9) - | +-- ChartIndicatorAdd(0, sub, g_hWMA45) - | - +-- DrawAllVisuals(g_LastTrend) (Visualizer.mqh) - vẽ dashboard + stats lần đầu -``` - -#### Bước 1. Mỗi tick — `OnTick()` (`RSIForceStateEA.mq5`) - -``` -RSIForceStateEA.mq5 :: OnTick() - | - +-- RefreshIndicatorData() (Indicators.mqh) - | cập nhật g_Bars / g_RSI / g_EMA9 / g_WMA45 / g_EMA200 / g_ATR - | nếu copy lỗi (ví dụ thị trường vừa mở) -> return, chờ tick sau - | - +-- SyncStateWithBroker() (StateMachine.mqh) - | | - | nếu state = PENDING_ORDER: - | +- HasOurOpenPosition() = true ? (Trade.mqh) - | | -> chuyển IN_TRADE, copy kế hoạch từ g_Pending sang g_OpenTrade - | +- HasOurPendingOrder() = false (hủy/hết hạn/từ chối) ? - | -> reset g_HasTradedThisPullback, về NO_TRADE - | - | nếu state = IN_TRADE: - | +- HasOurOpenPosition() = false (TP/SL/đóng tay) ? - | -> reset g_HasTradedThisPullback, về NO_TRADE - | - +-- if (state == IN_TRADE): - | ManagePartialAndBreakEven(g_OpenTrade) (Trade.mqh) - | | - | +- PositionSelectByTicket(positionTicket) - | +- profitDist = giá hiện tại so với giá mở (BUY/SELL) - | +- initRisk = |openPrice - SL| (lấy từ broker, không dùng plan) - | +- nếu profitDist >= InpPartialCloseAtR * initRisk: - | - PositionClosePartial(ticket, % volume) nếu chia lot được - | - PositionModify(ticket, openPrice, tp) — SL về BE - | - g_OpenTrade.partialClosedDone = true (chỉ một lần) - | - +-- newBar = IsNewBar() (StateMachine.mqh) - | so sánh g_Bars[0].time với lastBarTime — true khi nến mới mở - | - +-- if (newBar): RunStateMachine() <-- xem Bước 2 - | - +-- if (newBar HOẶC đã qua 2 giây kể từ lần vẽ trước): - DrawAllVisuals(DetectTrend(InpSignalBarShift)) (Visualizer.mqh) - | - +- DrawDashboardPanel(trend) - +- DrawStatsPanel() - +- DrawTradeLevels() -``` - -#### Bước 2. Có nến mới — `RunStateMachine()` (`StateMachine.mqh`) - -``` -StateMachine.mqh :: RunStateMachine() - | - +-- signalShift = InpSignalBarShift (mặc định = 1, nến vừa đóng) - +-- trendNow = DetectTrend(signalShift) (Up / Down / None) - | - +-- Phát hiện đảo chiều **có hướng** thật (UP <-> DOWN): - | | - | nếu trendNow != NONE: - | +- so với g_LastDirTrend (xu hướng có hướng trước đó) - | +- nếu g_LastDirTrend khác hướng và cả hai đều không NONE - | -> dirFlipped = true - | +- cập nhật g_LastDirTrend = trendNow - | - +-- if (dirFlipped): <-- chỉ reset khi UP <-> DOWN - | +- ResetPullbackCycle() - | +- nếu PENDING_ORDER -> CancelPendingOrder, về NO_TRADE - | +- nếu WATCHING -> về NO_TRADE - | - +-- g_LastTrend = trendNow - | - +-- switch (g_State): - | - case STATE_NO_TRADE: - | HandleStateNoTrade(...) - | - case STATE_WATCHING: - | HandleStateWatching(...) - | (timeout, BuildSignalPlan, PlaceLimitOrderFromPlan...) - | - case STATE_PENDING_ORDER: - | TickPendingOrderLifecycle(...) - | (đếm nến, hết hạn/invalid -> hủy, reset anti-spam) - | - case STATE_IN_TRADE: - (không làm gì ở đây — partial/BE chạy trong OnTick) -``` - -#### Bước 3. Biến động lệnh — `OnTradeTransaction()` - -``` -RSIForceStateEA.mq5 :: OnTradeTransaction(...) - | - +-- SyncStateWithBroker() - đồng bộ ngay khi broker báo sự kiện (không chỉ dựa vào tick) -``` - -#### Bước 4. Gỡ EA — `OnDeinit()` - -``` -RSIForceStateEA.mq5 :: OnDeinit(reason) - | - +-- RemoveAllVisuals() (Visualizer.mqh) - | ObjectsDeleteAll(0, "RSIForce_") - | - +-- ReleaseIndicators() (Indicators.mqh) - IndicatorRelease cho mọi handle -``` - -### 7.3 Bảng phân công: file nào làm việc gì - -| Trách nhiệm | File | Hàm chính | -| ----------- | ---- | --------- | -| Toàn bộ input | `Config.mqh` | (chỉ khai báo) | -| Enum / struct | `State.mqh` | `EAState`, `TrendDirection`, `SignalSnapshot`, `PendingContext`, `TradeContext` | -| Tạo & cập nhật chỉ báo | `Indicators.mqh` | `InitIndicators`, `RefreshIndicatorData`, `IsBufferSlopingDown/Up`, `HasCrossInLastNBars` | -| Lot, đặt/hủy/quản lý lệnh | `Trade.mqh` | `CalcLotsForRisk`, `PlaceLimitOrderFromPlan`, `CancelPendingOrder`, `ManagePartialAndBreakEven` | -| Trend, pullback, trigger | `StateMachine.mqh` | `DetectTrend`, `IsPullbackInUptrend/Downtrend`, `IsBuyTriggerSignal`, `IsSellTriggerSignal` | -| Điều phối state machine | `StateMachine.mqh` | `RunStateMachine`, `HandleStateNoTrade`, `HandleStateWatching`, `TickPendingOrderLifecycle` | -| Đồng bộ broker | `StateMachine.mqh` | `SyncStateWithBroker` (+ `HasOurOpenPosition` / `HasOurPendingOrder` trong `Trade.mqh`) | -| UI & mức giá | `Visualizer.mqh` | `DrawDashboardPanel`, `DrawStatsPanel`, `DrawTradeLevels`, `AttachIndicatorsToChart` | -| Vòng đời & validate | `RSIForceStateEA.mq5` | `OnInit`, `OnTick`, `OnTradeTransaction`, `OnDeinit`, `ValidateInputs` | - -### 7.4 Global của EA và ai được sửa - -| Global | Kiểu | Vai trò | Thường sửa tại | -| ------ | ---- | ------- | -------------- | -| `g_State` | `EAState` | Trạng thái SM | `TransitionTo` (StateMachine.mqh) | -| `g_Pending` | `PendingContext` | Lệnh chờ | `PlaceLimitOrderFromPlan`, `CancelPendingOrder` | -| `g_OpenTrade` | `TradeContext` | Vị thế mở | `SyncStateWithBroker`, `ManagePartialAndBreakEven` | -| `g_HasTradedThisPullback` | `bool` | Chống spam | Khi đặt lệnh; reset khi hủy/đóng/đảo chiều | -| `g_BarsInWatching` | `int` | Timeout WATCHING | `HandleStateWatching` và khi chuyển trạng thái | -| `g_LastTrend` | `TrendDirection` | Trend mới nhất | Cuối `RunStateMachine` | -| `g_LastDirTrend` | `TrendDirection` | Trend có hướng gần nhất | `RunStateMachine` khi `trendNow != NONE` | - -### 7.5 Sự kiện broker → cách code phản ứng - -| Sự kiện thực tế | Cách phát hiện | Phản ứng trong code | -| --------------- | -------------- | ------------------- | -| Limit khớp | `OnTradeTransaction` + `SyncStateWithBroker` | `PENDING` → `IN_TRADE`, copy plan | -| Pending hết hạn / bị hủy | `Sync` (không còn order) | `PENDING` → `NO_TRADE`, reset anti-spam | -| Pending bị từ chối | `PlaceLimit...` false hoặc `Sync` | Ở `NO_TRADE`, có log retcode | -| Chạm TP | `Sync` (không còn position) | `IN_TRADE` → `NO_TRADE`, reset anti-spam | -| Chạm SL | như trên | như trên | -| Đóng tay | như trên | như trên | -| Đạt mức partial R | `OnTick` → `ManagePartialAndBreakEven` | Partial (nếu được) + SL về BE | -| Đảo chiều UP ↔ DOWN | `RunStateMachine` — `dirFlipped` | Hủy pending ngược hướng, reset chu kỳ | -| Đi qua NONE rồi về cùng hướng | `RunStateMachine` | **Không** coi là flip; `g_LastDirTrend` giữ ngữ cảnh | -| Nến mới | `OnTick` → `IsNewBar` | Gọi `RunStateMachine` một lần | - -## 8) Ghi chú kiến trúc - -- State machine chạy theo **nến mới** (`IsNewBar()`), tránh bắn tín hiệu nhiều lần trong cùng một nến. -- Quản lý vị thế (partial + BE) chạy **mỗi tick** để phản ứng nhanh theo giá. -- `SyncStateWithBroker()` chạy mỗi tick và sau `OnTradeTransaction` để trạng thái nội bộ luôn khớp broker (đóng tay, từ chối lệnh, v.v.). -- Phân tầng `#include`: - - **Tầng 1** chỉ khai báo dữ liệu và hàm thuần. - - **Tầng 2** dùng các global khai báo giữa hai tầng. - - **Tầng 3** chỉ đọc/hiển thị, không sửa logic nghiệp vụ cốt lõi. - ---- - -*Tệp README dùng mã hóa UTF-8. Tiếng Việt có dấu an toàn với Git, GitHub và trình soạn thảo hiện đại; không ảnh hưởng tới biên dịch EA (MetaEditor không compile file `.md`).* diff --git a/Experts/RSIForceStateEA/State.mqh b/Experts/RSIForceStateEA/State.mqh deleted file mode 100644 index 6820685..0000000 --- a/Experts/RSIForceStateEA/State.mqh +++ /dev/null @@ -1,52 +0,0 @@ -#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 deleted file mode 100644 index 5d35023..0000000 --- a/Experts/RSIForceStateEA/StateMachine.mqh +++ /dev/null @@ -1,371 +0,0 @@ -#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 deleted file mode 100644 index 90400f3..0000000 --- a/Experts/RSIForceStateEA/Trade.mqh +++ /dev/null @@ -1,328 +0,0 @@ -#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 deleted file mode 100644 index 2960aa2..0000000 --- a/Experts/RSIForceStateEA/Visualizer.mqh +++ /dev/null @@ -1,366 +0,0 @@ -#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