diff --git a/Experts/RsiMomentumEA_README.md b/Experts/RsiMomentumEA_README.md new file mode 100644 index 0000000..4738c9f --- /dev/null +++ b/Experts/RsiMomentumEA_README.md @@ -0,0 +1,428 @@ +# RsiMomentumEA — Tài liệu logic + +**Phiên bản EA:** 4.14 +**File chính:** `Experts/RsiMomentumEA.mq5` +**Module:** `Include/RsiMom/PhaseEntry.mqh`, `SignalDebug.mqh`, `TradeJournal.mqh` + +EA giao dịch theo **động lượng RSI** (RSI + EMA9 + WMA45 trên cùng thang RSI), kết hợp **chuỗi 5 phase** trước khi RSI cắt WMA45. Logic **độc lập** với indicator `RsiMomentumIndicator` (không đọc buffer indicator ngoài). + +--- + +## 1. Tổng quan kiến trúc + +```mermaid +flowchart TB + subgraph calc [Mỗi tick — OnCalculate] + IND[Copy RSI / EMA9 / WMA45 / EMA200] + SCAN[SignalScan_Run — quét nến] + BUF[buf_Signal shift] + end + subgraph trade [Mỗi nến mới — OnTick] + TRY[TradeTryOnBarOpen] + EXE[TradeExecuteOrder — Buy/Sell Limit] + PM[Position_ManageAt1R] + PEND[Pending expiry / env cancel] + end + IND --> SCAN --> BUF + BUF --> TRY --> EXE + EXE --> PM + EXE --> PEND +``` + +| Thành phần | Vai trò | +|------------|---------| +| `buf_RSI`, `buf_EMA9`, `buf_WMA45` | Giá trị chỉ báo tại từng `shift` (series) | +| `buf_Signal` | `+1` BUY hợp lệ, `-1` SELL hợp lệ, `0` không tín hiệu | +| `buf_Trend` | `+1` / `-1` / `0` theo EMA200 (panel, không bắt buộc cho entry nếu tắt filter) | +| Mũi tên `RsiMomEA_CR_*` | Chỉ vẽ khi tín hiệu **đủ điều kiện** (pass toàn pipeline tín hiệu) | +| Mũi tên `RsiMomEA_DBG_*` | Mọi **cross RSI×WMA45** + lý do OK/SKIP (debug) | + +--- + +## 2. Chỉ báo + +Tất cả tính trên **khung thời gian chart** EA gắn (`_Symbol`, `_Period`). + +| Handle | Công thức MT5 | Ghi chú | +|--------|---------------|---------| +| `h_RSI` | `iRSI(..., PRICE_CLOSE)` | RSI(14) mặc định | +| `h_EMA9` | `iMA(..., MODE_EMA, h_RSI)` | EMA9 **của RSI** | +| `h_WMA45` | `iMA(..., MODE_LWMA, h_RSI)` | WMA45 **của RSI** | +| `h_EMA200` | `iMA(..., PRICE_CLOSE)` | Trend trên **giá**, không phải RSI | +| `h_ATR_Regime` | ATR(14) | Bộ lọc ATR mở rộng (tùy chọn) | +| `h_ATR` | ATR(14) | Buffer SL sau swing | + +**Quy ước index:** `shift = 1` là nến **vừa đóng**; `shift = 0` là nến đang hình thành. EA quét tín hiệu trên nến đã đóng (`i >= 1`). + +--- + +## 3. Logic vào lệnh — tổng thể + +Entry chỉ xảy ra khi **hai tầng** đều pass: + +1. **Tầng tín hiệu** (`SignalScan_Run`) → ghi `buf_Signal[InpSignalBarShift]` (mặc định `1`). +2. **Tầng thực thi** (`TradeExecuteOrder` khi mở nến mới) → phiên/spread, Limit hợp lệ, SL/TP, volume. + +### 3.1. Trigger bắt buộc (mọi lệnh) + +Đánh giá tại nến `i` (thường `i = 1`). + +**BUY — cross lên WMA45:** + +``` +RSI[i+1] <= WMA45[i+1] AND RSI[i] > WMA45[i] +``` + +**SELL — cross xuống WMA45:** + +``` +RSI[i+1] >= WMA45[i+1] AND RSI[i] < WMA45[i] +``` + +Không có cross → không xét tiếp. + +### 3.2. Lõi entry: 5 phase hoặc EMA9 đơn giản + +| `InpPhaseFilterEnabled` | Điều kiện lõi | +|-------------------------|----------------| +| `true` (mặc định) | `Phase_BuyPasses` / `Phase_SellPasses` — đủ P1→P5 (mục 4) | +| `false` | BUY: `EMA9[i] < WMA45[i]` — SELL: `EMA9[i] > WMA45[i]` | + +### 3.3. Bộ lọc tín hiệu (tùy chọn, có thể tắt) + +| Input | Mặc định (debug) | Điều kiện khi **bật** | +|-------|------------------|------------------------| +| `InpTrendFilterEnabled` | `false` | BUY: `InpTrendConfirmBars` nến liên tiếp có `Close > EMA200`. SELL: `Close < EMA200`. | +| `InpAtrExpFilterEnabled` | `false` | ATR(shift) / ATR(shift+cmp) ≥ `InpAtrExpMinRatio` **và** ATR tăng liên tiếp `InpAtrExpRiseBars` nến. | +| `InpSessionFilterEnabled` | `false` | Thời gian **nến tín hiệu** nằm London hoặc NY; không trong cửa sổ giao phiên; trừ `InpSessionAvoidLastMin` phút cuối phiên. | + +**Tín hiệu hợp lệ (ghi `buf_Signal`):** + +``` +validBuy = crossUp AND coreBuy AND trendUp AND atrOk AND sessionOk +validSell = crossDn AND coreSell AND trendDn AND atrOk AND sessionOk +``` + +(`trendUp` / `atrOk` / `sessionOk` luôn `true` nếu filter tương ứng **tắt**.) + +### 3.4. Tầng thực thi (đặt lệnh) + +Khi **nến mới** mở (`OnTick`, `t0 != g_tradeBarAnchor`): + +- Đọc `buf_Signal[1]`. +- Nếu `InpTradeEnabled` và `buf_Signal[1] == ±1` → `TradeExecuteOrder`. + +**Thêm kiểm tra tại thời điểm đặt lệnh** (`Env_AllowsTradeAtBar`): + +- Phiên + giao phiên (nếu `InpSessionFilterEnabled`). +- Spread ≤ `InpMaxSpreadPoints` (nếu `InpSpreadFilterEnabled`; trong Tester thường bỏ qua nếu `InpSpreadSkipInTester = true`). + +**Không** đặt lệnh nếu: + +- `InpOnePositionFlat` và đã có position/pending cùng magic. +- Limit @ 50% body không hợp lệ so với Bid/Ask + `STOPS_LEVEL`. +- Không tính được SL/TP swing hoặc volume = 0. + +--- + +## 4. Entry 5 phase — chi tiết + +Implement trong `Include/RsiMom/PhaseEntry.mqh`. Mô tả theo **BUY**; **SELL** đối xứng (mở rộng lên, EMA9 xuống, v.v.). + +Quét trên các nến **đã đóng** trong lookback: từ `shift+1` trở về quá khứ (index series tăng = quá khứ). + +### Phase 1 — Mở rộng (expansion) + +**Ý nghĩa:** Trước đó ba đường RSI / EMA9 / WMA45 đã **xếp lớp** và giãn đủ mạnh (sóng động lượng). + +**BUY:** Trong `InpPhaseExpandLookback` nến (mặc định 25), tìm nến `j` thỏa: + +``` +RSI[j] < EMA9[j] < WMA45[j] +spread[j] = WMA45[j] - RSI[j] → max(spread) >= InpPhaseMinExpandSpread (mặc định 8 điểm RSI) +``` + +**SELL:** `RSI[j] > EMA9[j] > WMA45[j]`, `spread = RSI[j] - WMA45[j]`. + +Fail tag: `P1-mở rộng`. + +### Phase 2 — Cuộn quanh EMA9 (coil) + +**Ý nghĩa:** RSI không chỉ **xuyên** EMA9 một lần mà **đan** qua lại (chuẩn bị trước cắt WMA45). + +Trong `InpPhaseCoilLookback` nến (mặc định 12), đếm số lần RSI cắt EMA9 (lên hoặc xuống): + +``` +cross tại j nếu: + (RSI[j+1] <= EMA9[j+1] && RSI[j] > EMA9[j]) OR + (RSI[j+1] >= EMA9[j+1] && RSI[j] < EMA9[j]) +``` + +Yêu cầu: `số_cross >= InpPhaseMinRsiEma9Cross` (mặc định **2**). + +> **Lưu ý:** `InpPhaseCoilBand` được khai báo trong input nhưng **chưa dùng** trong `Phase_BuyPasses` (chỉ đếm cross). Hàm `Phase_CountCoilBars` có sẵn để mở rộng sau. + +Fail tag: `P2-cuộn`. + +### Phase 3 — EMA9 quay hướng (đã nới lỏng) + +**BUY:** EMA9 **không được giảm quá mạnh** trong cửa sổ `InpPhaseEma9SlopeBars` (mặc định **2**): + +``` +EMA9[i] >= EMA9[i + bars] - InpPhaseEma9SlopeTol (tol mặc định 1.5 pt RSI) +``` + +Cho phép EMA9 **phẳng hoặc hơi lệch** thay vì bắt buộc tăng rõ. + +**SELL:** `EMA9[i] <= EMA9[i + bars] + tol`. + +Fail tag: `P3-EMA9↑` / `P3-EMA9↓`. + +### Phase 4 — WMA45 phẳng lại + +**Ý nghĩa:** WMA45 trước đó dốc theo hướng sóng; tại nến tín hiệu đã **làm phẳng** (sắp cắt). + +Với `b = InpPhaseWmaFlatBars` (mặc định 5): + +``` +recent = (WMA45[i] - WMA45[i+b]) / b +prior = (WMA45[i+b] - WMA45[i+2b]) / b +``` + +**BUY:** + +- `|recent| <= InpPhaseWmaFlatMaxSlope` (mặc định **0.55** — lỏng hơn 0.35 cũ). +- Quá khứ WMA45: + - `InpPhaseWmaRelaxPrior = true` (mặc định): chỉ cần `prior < 0` (từng giảm, không cần dốc mạnh). + - `false`: cần `prior <= -InpPhaseWmaWasSlopeMin` (mặc định **0.08**). + +**SELL:** đối xứng (`prior > 0` khi relax). + +Fail tag: `P4-WMA45 phẳng`. + +### Phase 5 — EMA9 gần WMA45 tại cắt + +**Ý nghĩa:** Cross xảy ra **gần** WMA45, không phải RSI đã bứt xa (tín hiệu yếu / muộn). + +**BUY:** + +``` +EMA9[i] < WMA45[i] +(WMA45[i] - EMA9[i]) <= InpPhaseMaxEma9WmaGap (mặc định **18** điểm RSI) +``` + +**SELL:** `EMA9[i] > WMA45[i]`, `(EMA9[i] - WMA45[i]) <= gap`. + +Fail tag: `P5-EMA9 xa WMA45`. + +### Thứ tự kiểm tra trong code + +P1 → P2 → P3 → P4 → P5 (fail fast, `failTag` ghi phase đầu tiên lỗi). + +**Cross WMA45 (trigger) không nằm trong P5** — luôn kiểm tra **trước** trong `SignalScan_Run`. + +--- + +## 5. Sơ đồ chuỗi BUY (hình dung) + +```mermaid +sequenceDiagram + participant P1 as P1 Mở rộng + participant P2 as P2 Cuộn EMA9 + participant P3 as P3 EMA9 lên + participant P4 as P4 WMA45 phẳng + participant P5 as P5 EMA9 gần WMA45 + participant X as Cross RSI lên WMA45 + participant F as Filter tùy chọn + participant T as Trade Limit + + P1->>P2: quá khứ 12-25 nến + P2->>P3: tại nến i + P3->>P4: tại nến i + P4->>P5: tại nến i + P5->>X: cùng nến i + X->>F: EMA200 / ATR / phiên + F->>T: buf_Signal=1, nến sau +``` + +--- + +## 6. Thực thi lệnh + +### 6.1. Loại lệnh và giá vào + +- **Buy Limit / Sell Limit** tại **50% thân nến tín hiệu**: + +``` +entry = (Open[InpSignalBarShift] + Close[InpSignalBarShift]) / 2 +``` + +- BUY Limit: giá **dưới** Ask (có khoảng cách `STOPS_LEVEL` / `FREEZE_LEVEL`). +- SELL Limit: giá **trên** Bid. + +### 6.2. Stop loss + +- Tìm **swing pivot** gần nhất trong `InpSwingMaxBars` (mặc định 30): + - BUY: đáy pivot (low thấp hơn 2 nến kề). + - SELL: đỉnh pivot. +- Nếu không có pivot: min/max low/high trong cửa sổ. +- SL = pivot ± **buffer**: + - `spread × Point` nếu `InpSlAtrAddSpread`. + - `+ ATR(1) × InpSlAtrMultiplier` nếu `InpSlAtrBufferEnabled`. + +### 6.3. Take profit + +``` +risk = |entry - SL| +TP = entry ± risk × InpRewardRiskRatio (mặc định 1.1 R) +``` + +### 6.4. Khối lượng + +``` +riskMoney = Balance × (InpRiskPercent / 100) +volume = riskMoney / lossPerLot(1 lot từ entry → SL) +``` + +Làm tròn theo `SYMBOL_VOLUME_STEP`, tối thiểu `SYMBOL_VOLUME_MIN`. + +### 6.5. Pending + +| Hành vi | Input | +|---------|--------| +| Hủy sau N nến không khớp | `InpLimitExpireBars` (40) | +| Hủy khi phiên/spread xấu | `Pending_EnvCancelIfBad` | +| Hủy pending cũ trước khi đặt mới | `Pending_CancelMine` trong `TradeExecuteOrder` | +| Một vị thế / một pending | `InpOnePositionFlat` | + +--- + +## 7. Quản lý lệnh mở + +**`InpManageAt1R` (mặc định bật):** + +Khi lợi nhuận floating ≥ **1R** (R = khoảng cách entry → SL lúc mở lệnh): + +1. Chốt `InpPartialCloseRatio` volume (mặc định 50%). +2. Dời SL về entry ± `InpBreakevenOffsetPts`. + +Chỉ thực hiện một lần mỗi ticket (`g_pmAt1RDone`). + +--- + +## 8. Debug trên chart + +| Input | Mô tả | +|-------|--------| +| `InpDebugMarkSignals` | Vẽ `RsiMomEA_DBG_*` tại **mọi** cross RSI×WMA45 | +| `InpDebugMarkMaxBars` | Giới hạn N nến gần nhất | +| `InpDebugLogExperts` | In chi tiết tại `InpSignalBarShift` | + +**Màu / nhãn:** + +| Hiển thị | Ý nghĩa | +|----------|---------| +| Xanh **OK** | Pass tín hiệu + pass đặt lệnh (tại nến signal) | +| Vàng **SIG** | Pass tín hiệu, không đặt (trade tắt / env) | +| Đỏ **SKIP** | Cross nhưng fail P1–P5 hoặc filter | + +Tooltip: `RSI↑WMA45 | P1:OK | P2:FAIL(x=1) | … | EMA200:OFF | ATR:OFF | Phiên:OFF | Đặt lệnh:OK`. + +Logic đánh giá: `Include/RsiMom/SignalDebug.mqh`. + +--- + +## 9. Phiên & spread (khi bật lại) + +**Phiên** (`InpSessionFilterEnabled = true`): + +- London: `InpLondonStartHour`–`InpLondonEndHour` (mặc định 8–17). +- New York: `InpNYStartHour`–`InpNYEndHour` (13–22). +- Giờ: **server broker** nếu `InpEnvUseUtc = false`, ngược lại quy đổi UTC. +- Trừ `InpSessionAvoidLastMin` phút cuối mỗi cửa sổ. +- `InpTransitionBlockEnabled`: chặn thêm cửa sổ 7–8h và 21–22h (giao phiên). + +Khi `InpSessionFilterEnabled = false`: **bỏ toàn bộ** lọc phiên và giao phiên. + +**Spread:** `SYMBOL_SPREAD` (points) ≤ `InpMaxSpreadPoints`. Tester: thường dùng `InpSpreadSkipInTester = true`. + +--- + +## 10. Journal CSV + +`InpExportTradeJournal`: + +- `FILE_COMMON/RsiMomEA/journal_SYMBOL_PERIOD.csv` — từng lệnh đóng. +- `summary_SYMBOL_PERIOD.csv` — tổng hợp theo tháng. + +`InpJournalResetOnInit`: xóa CSV cũ mỗi lần chạy backtest mới trong Tester. + +--- + +## 11. Cấu trúc file + +``` +Experts/ + RsiMomentumEA.mq5 # EA chính, inputs, scan, trade + RsiMomentumEA_README.md # Tài liệu này + +Include/RsiMom/ + PhaseEntry.mqh # P1–P5 + SignalDebug.mqh # Đánh dấu + checklist tooltip + TradeJournal.mqh # Xuất CSV +``` + +--- + +## 12. Preset gợi ý + +### Debug 5 phase (mặc định hiện tại) + +``` +InpPhaseFilterEnabled = true +InpTrendFilterEnabled = false +InpAtrExpFilterEnabled = false +InpSessionFilterEnabled = false +InpDebugMarkSignals = true +``` + +Chỉ còn: **cross RSI×WMA45 + P1–P5** (+ spread nếu bật và không skip tester). + +### Live / backtest đầy đủ + +``` +InpTrendFilterEnabled = true +InpTrendConfirmBars = 1 +InpAtrExpFilterEnabled = true +InpSessionFilterEnabled = true +InpSpreadFilterEnabled = true +``` + +--- + +## 13. Khác biệt với indicator + +EA **không** đọc file indicator. Indicator `Indicators/RsiMomentumIndicator` có thể có thêm rule (EMA9 persist, slope RSI, OB/OS) chưa port sang EA — khi so sánh chart indicator vs EA cần kiểm tra từng input tương ứng. + +--- + +## 14. Tham chiếu nhanh input entry + +| Input | Mặc định | Phase / vai trò | +|-------|----------|-----------------| +| `InpPhaseExpandLookback` | 25 | P1 | +| `InpPhaseMinExpandSpread` | 8.0 | P1 | +| `InpPhaseCoilLookback` | 12 | P2 | +| `InpPhaseMinRsiEma9Cross` | 2 | P2 | +| `InpPhaseEma9SlopeBars` | 2 | P3 | +| `InpPhaseEma9SlopeTol` | 1.5 | P3 | +| `InpPhaseWmaFlatBars` | 4 | P4 | +| `InpPhaseWmaWasSlopeMin` | 0.08 | P4 (khi RelaxPrior=false) | +| `InpPhaseWmaFlatMaxSlope` | 0.55 | P4 | +| `InpPhaseWmaRelaxPrior` | true | P4 | +| `InpPhaseMaxEma9WmaGap` | 18.0 | P5 | +| `InpSignalBarShift` | 1 | Nến tín hiệu & Limit | + +--- + +*Tài liệu đồng bộ với mã nguồn v4.14. P1–P2 giữ nguyên; P3–P5 đã nới lỏng mặc định.* diff --git a/Include/RsiMom/PhaseEntry.mqh b/Include/RsiMom/PhaseEntry.mqh new file mode 100644 index 0000000..c9a745e --- /dev/null +++ b/Include/RsiMom/PhaseEntry.mqh @@ -0,0 +1,266 @@ +//+------------------------------------------------------------------+ +//| PhaseEntry.mqh — entry 5 phase (mở rộng → cuộn → EMA9↑ → WMA45 phẳng → cắt) | +//+------------------------------------------------------------------+ +#ifndef RSI_MOM_PHASE_ENTRY_MQH +#define RSI_MOM_PHASE_ENTRY_MQH + +struct PhaseEntryConfig +{ + bool enabled; + int expandLookback; // quét phase 1 trong [shift+1 .. shift+N] + double minExpandSpread; // min (WMA45-RSI) khi xếp lớp bear/bull + int coilLookback; // quét cuộn RSI↔EMA9 trước nến tín hiệu + int minRsiEma9Crosses; // số lần RSI cắt EMA9 (chống xuyên 1 lần — dấu hiệu 3) + double coilBand; // |RSI-EMA9| <= band tính là quanh EMA9 + int ema9SlopeBars; // cửa sổ so sánh hướng EMA9 (P3) + double ema9SlopeTol; // BUY: cho phép EMA9 giảm tối đa X pt RSI trong cửa sổ P3 + int wmaFlatBars; // |WMA45[i]-WMA45[i+bars]| <= flatMax + double wmaWasSlopeMin; // P4: WMA45 trước đó dốc đủ (khi wmaRelaxPrior=false) + double wmaFlatMaxSlope; // WMA45 gần phẳng tại signal + bool wmaRelaxPrior; // P4: chỉ cần WMA45 từng giảm/tăng, không cần dốc tối thiểu + double maxEma9WmaGap; // EMA9 gần WMA45 tại cắt (chống dấu hiệu 1) +}; + +bool Phase_Ema9TurningBuy(const int shift, const PhaseEntryConfig &cfg, const double &ema9[]) +{ + const int b = cfg.ema9SlopeBars; + if(ema9[shift + b] <= 0.0) + return false; + return (ema9[shift] >= ema9[shift + b] - cfg.ema9SlopeTol); +} + +bool Phase_Ema9TurningSell(const int shift, const PhaseEntryConfig &cfg, const double &ema9[]) +{ + const int b = cfg.ema9SlopeBars; + if(ema9[shift + b] <= 0.0) + return false; + return (ema9[shift] <= ema9[shift + b] + cfg.ema9SlopeTol); +} + +int Phase_CountRsiEma9Crosses(const int shift, const int lookback, const int rates_total, + const double &rsi[], const double &ema9[]) +{ + int crosses = 0; + const int end = MathMin(shift + lookback, rates_total - 2); + for(int j = shift + 1; j < end; j++) + { + const bool up = (rsi[j+1] <= ema9[j+1]) && (rsi[j] > ema9[j]); + const bool dn = (rsi[j+1] >= ema9[j+1]) && (rsi[j] < ema9[j]); + if(up || dn) + crosses++; + } + return crosses; +} + +int Phase_CountCoilBars(const int shift, const int lookback, const int rates_total, + const double &rsi[], const double &ema9[], const double band) +{ + int n = 0; + const int end = MathMin(shift + lookback, rates_total - 1); + for(int j = shift + 1; j <= end; j++) + { + if(MathAbs(rsi[j] - ema9[j]) <= band) + n++; + } + return n; +} + +bool Phase_FindMaxSpreadDown(const int shift, const int lookback, const int rates_total, + const double &rsi[], const double &ema9[], const double &wma[], + const double minSpread, double &maxSpread) +{ + maxSpread = 0.0; + const int end = MathMin(shift + lookback, rates_total - 1); + for(int j = shift + 1; j <= end; j++) + { + if(rsi[j] >= ema9[j] || ema9[j] >= wma[j]) + continue; + const double sp = wma[j] - rsi[j]; + if(sp > maxSpread) + maxSpread = sp; + } + return (maxSpread >= minSpread - 1e-8); +} + +bool Phase_FindMaxSpreadUp(const int shift, const int lookback, const int rates_total, + const double &rsi[], const double &ema9[], const double &wma[], + const double minSpread, double &maxSpread) +{ + maxSpread = 0.0; + const int end = MathMin(shift + lookback, rates_total - 1); + for(int j = shift + 1; j <= end; j++) + { + if(rsi[j] <= ema9[j] || ema9[j] <= wma[j]) + continue; + const double sp = rsi[j] - wma[j]; + if(sp > maxSpread) + maxSpread = sp; + } + return (maxSpread >= minSpread - 1e-8); +} + +bool Phase_Wma45FlatteningBuyEx(const int shift, const int flatBars, + const double flatMaxSlope, const double wasSlopeMin, + const bool relaxPrior, const double &wma[]) +{ + const int b = MathMax(2, flatBars); + const double recent = (wma[shift] - wma[shift + b]) / (double)b; + const double prior = (wma[shift + b] - wma[shift + 2 * b]) / (double)b; + if(relaxPrior) + { + if(prior > 0.0) + return false; + } + else if(prior > -wasSlopeMin) + return false; + return (MathAbs(recent) <= flatMaxSlope); +} + +bool Phase_Wma45FlatteningBuy(const int shift, const int flatBars, + const double flatMaxSlope, const double wasSlopeMin, + const double &wma[]) +{ + return Phase_Wma45FlatteningBuyEx(shift, flatBars, flatMaxSlope, wasSlopeMin, false, wma); +} + +bool Phase_Wma45FlatteningSellEx(const int shift, const int flatBars, + const double flatMaxSlope, const double wasSlopeMin, + const bool relaxPrior, const double &wma[]) +{ + const int b = MathMax(2, flatBars); + const double recent = (wma[shift] - wma[shift + b]) / (double)b; + const double prior = (wma[shift + b] - wma[shift + 2 * b]) / (double)b; + if(relaxPrior) + { + if(prior < 0.0) + return false; + } + else if(prior < wasSlopeMin) + return false; + return (MathAbs(recent) <= flatMaxSlope); +} + +bool Phase_Wma45FlatteningSell(const int shift, const int flatBars, + const double flatMaxSlope, const double wasSlopeMin, + const double &wma[]) +{ + return Phase_Wma45FlatteningSellEx(shift, flatBars, flatMaxSlope, wasSlopeMin, false, wma); +} + +bool Phase_Wma45FlatteningBuyCfg(const int shift, const PhaseEntryConfig &cfg, const double &wma[]) +{ + return Phase_Wma45FlatteningBuyEx(shift, cfg.wmaFlatBars, cfg.wmaFlatMaxSlope, + cfg.wmaWasSlopeMin, cfg.wmaRelaxPrior, wma); +} + +bool Phase_Wma45FlatteningSellCfg(const int shift, const PhaseEntryConfig &cfg, const double &wma[]) +{ + return Phase_Wma45FlatteningSellEx(shift, cfg.wmaFlatBars, cfg.wmaFlatMaxSlope, + cfg.wmaWasSlopeMin, cfg.wmaRelaxPrior, wma); +} + +bool Phase_Ema9NearWmaBuy(const int shift, const double maxGap, + const double &ema9[], const double &wma[]) +{ + if(ema9[shift] >= wma[shift]) + return false; + return ((wma[shift] - ema9[shift]) <= maxGap + 1e-8); +} + +bool Phase_Ema9NearWmaSell(const int shift, const double maxGap, + const double &ema9[], const double &wma[]) +{ + if(ema9[shift] <= wma[shift]) + return false; + return ((ema9[shift] - wma[shift]) <= maxGap + 1e-8); +} + +bool Phase_BuyPasses(const int shift, const int rates_total, + const double &rsi[], const double &ema9[], const double &wma[], + const PhaseEntryConfig &cfg, string &failTag) +{ + failTag = ""; + if(!cfg.enabled) + return true; + + double maxSp = 0.0; + if(!Phase_FindMaxSpreadDown(shift, cfg.expandLookback, rates_total, + rsi, ema9, wma, cfg.minExpandSpread, maxSp)) + { + failTag = "P1-mở rộng "; + return false; + } + + const int crosses = Phase_CountRsiEma9Crosses(shift, cfg.coilLookback, rates_total, rsi, ema9); + if(crosses < cfg.minRsiEma9Crosses) + { + failTag = "P2-cuộn "; + return false; + } + + if(!Phase_Ema9TurningBuy(shift, cfg, ema9)) + { + failTag = "P3-EMA9↑ "; + return false; + } + + if(!Phase_Wma45FlatteningBuyCfg(shift, cfg, wma)) + { + failTag = "P4-WMA45 phẳng "; + return false; + } + + if(!Phase_Ema9NearWmaBuy(shift, cfg.maxEma9WmaGap, ema9, wma)) + { + failTag = "P5-EMA9 xa WMA45 "; + return false; + } + + return true; +} + +bool Phase_SellPasses(const int shift, const int rates_total, + const double &rsi[], const double &ema9[], const double &wma[], + const PhaseEntryConfig &cfg, string &failTag) +{ + failTag = ""; + if(!cfg.enabled) + return true; + + double maxSp = 0.0; + if(!Phase_FindMaxSpreadUp(shift, cfg.expandLookback, rates_total, + rsi, ema9, wma, cfg.minExpandSpread, maxSp)) + { + failTag = "P1-mở rộng "; + return false; + } + + const int crosses = Phase_CountRsiEma9Crosses(shift, cfg.coilLookback, rates_total, rsi, ema9); + if(crosses < cfg.minRsiEma9Crosses) + { + failTag = "P2-cuộn "; + return false; + } + + if(!Phase_Ema9TurningSell(shift, cfg, ema9)) + { + failTag = "P3-EMA9↓ "; + return false; + } + + if(!Phase_Wma45FlatteningSellCfg(shift, cfg, wma)) + { + failTag = "P4-WMA45 phẳng "; + return false; + } + + if(!Phase_Ema9NearWmaSell(shift, cfg.maxEma9WmaGap, ema9, wma)) + { + failTag = "P5-EMA9 xa WMA45 "; + return false; + } + + return true; +} + +#endif diff --git a/Include/RsiMom/SignalDebug.mqh b/Include/RsiMom/SignalDebug.mqh new file mode 100644 index 0000000..5e3ab94 --- /dev/null +++ b/Include/RsiMom/SignalDebug.mqh @@ -0,0 +1,542 @@ +//+------------------------------------------------------------------+ +//| SignalDebug.mqh — đánh dấu cross RSI×WMA45: OK / SKIP + tooltip | +//+------------------------------------------------------------------+ +#ifndef RSI_MOM_SIGNAL_DEBUG_MQH +#define RSI_MOM_SIGNAL_DEBUG_MQH + +#include + +struct SignalEvalResult +{ + bool signalOk; + bool tradeOk; + string summary; + string detail; + string failTag; // lý do chính: P1-mở rộng, P2-cuộn, EMA200, ATR, … +}; + +void SignalEval_Append(string &detail, const string part) +{ + if(StringLen(detail) > 0) + detail += " | "; + detail += part; +} + +void SignalEval_SetFail(string &failTag, const string tag) +{ + if(StringLen(failTag) == 0 && StringLen(tag) > 0) + failTag = tag; +} + +string SignalEval_FormatTooltip(const bool isBuy, const SignalEvalResult &ev) +{ + const string side = isBuy ? "BUY" : "SELL"; + string tip = side + " @ " + ev.summary; + if(StringLen(ev.failTag) > 0 && !ev.signalOk) + tip += "\nFail: " + ev.failTag; + tip += "\n---\n" + ev.detail; + return tip; +} + +string SignalEval_ChartLabel(const SignalEvalResult &ev) +{ + if(ev.tradeOk) + return "OK"; + if(ev.signalOk) + return "SIG"; + if(StringLen(ev.failTag) > 0) + { + string tag = ev.failTag; + StringTrimRight(tag); + StringTrimLeft(tag); + return "SKIP " + tag; + } + return "SKIP"; +} + +SignalEvalResult Signal_EvaluateBuyAt(const int shift, const int rates_total, const int trendN, + const double &rsi[], const double &ema9[], const double &wma[], + const double &closeArr[], const double &ema200Arr[], + const PhaseEntryConfig &phaseCfg, + const bool phaseFilterEnabled, + const bool trendFilterEnabled, + const bool atrFilterEnabled, + const bool rsiObOsFilterEnabled, + const double rsiOverbought, + const double rsiOversold, + const bool sessionFilterEnabled, + const bool atrExpAtBar, + const bool sessionAtBar, + const string sessionFailWhy = "") +{ + SignalEvalResult r; + r.signalOk = false; + r.tradeOk = false; + r.summary = ""; + r.detail = ""; + r.failTag = ""; + + const bool cross = (shift + 1 < rates_total) && + (rsi[shift + 1] <= wma[shift + 1]) && (rsi[shift] > wma[shift + 1]); + if(!cross) + { + r.summary = "no cross"; + SignalEval_Append(r.detail, "RSI chưa cắt lên WMA45"); + return r; + } + + SignalEval_Append(r.detail, "RSI↑WMA45"); + + bool coreOk = true; + string phaseFail = ""; + if(phaseFilterEnabled) + { + coreOk = Phase_BuyPasses(shift, rates_total, rsi, ema9, wma, phaseCfg, phaseFail); + double maxSp = 0.0; + const bool p1 = Phase_FindMaxSpreadDown(shift, phaseCfg.expandLookback, rates_total, + rsi, ema9, wma, phaseCfg.minExpandSpread, maxSp); + const int xc = Phase_CountRsiEma9Crosses(shift, phaseCfg.coilLookback, rates_total, rsi, ema9); + const bool p3 = Phase_Ema9TurningBuy(shift, phaseCfg, ema9); + const bool p4 = Phase_Wma45FlatteningBuyCfg(shift, phaseCfg, wma); + const double gap = wma[shift] - ema9[shift]; + const bool p5 = Phase_Ema9NearWmaBuy(shift, phaseCfg.maxEma9WmaGap, ema9, wma); + + SignalEval_Append(r.detail, p1 ? StringFormat("P1:OK(sp=%.1f)", maxSp) : "P1:FAIL"); + SignalEval_Append(r.detail, StringFormat("P2:%s(x=%d)", xc >= phaseCfg.minRsiEma9Crosses ? "OK" : "FAIL", xc)); + SignalEval_Append(r.detail, p3 ? "P3:OK" : "P3:FAIL"); + SignalEval_Append(r.detail, p4 ? "P4:OK" : "P4:FAIL"); + SignalEval_Append(r.detail, p5 ? StringFormat("P5:OK(gap=%.1f)", gap) : StringFormat("P5:FAIL(gap=%.1f)", gap)); + + if(!p1) SignalEval_SetFail(r.failTag, "P1-mở rộng"); + else if(xc < phaseCfg.minRsiEma9Crosses) + SignalEval_SetFail(r.failTag, "P2-cuộn"); + else if(!p3) SignalEval_SetFail(r.failTag, "P3-EMA9↑"); + else if(!p4) SignalEval_SetFail(r.failTag, "P4-WMA45 phẳng"); + else if(!p5) SignalEval_SetFail(r.failTag, "P5-EMA9 xa WMA45"); + else if(!coreOk && StringLen(phaseFail) > 0) + SignalEval_SetFail(r.failTag, phaseFail); + } + else + { + coreOk = (ema9[shift] < wma[shift]); + SignalEval_Append(r.detail, coreOk ? "EMA9=WMA45 FAIL"); + if(!coreOk) + SignalEval_SetFail(r.failTag, "EMA9>=WMA45"); + } + + bool trendUp = true; + if(!trendFilterEnabled) + SignalEval_Append(r.detail, "EMA200:OFF"); + else + { + for(int k = 0; k < trendN; k++) + { + const int idx = shift + k; + if(closeArr[idx] <= ema200Arr[idx]) + { + trendUp = false; + break; + } + } + SignalEval_Append(r.detail, trendUp ? "EMA200:OK" : "EMA200:FAIL"); + if(!trendUp) + SignalEval_SetFail(r.failTag, "EMA200"); + } + + SignalEval_Append(r.detail, !atrFilterEnabled ? "ATR:OFF" : (atrExpAtBar ? "ATR:OK" : "ATR:FAIL")); + if(atrFilterEnabled && !atrExpAtBar) + SignalEval_SetFail(r.failTag, "ATR co"); + + bool rsiOk = true; + if(!rsiObOsFilterEnabled) + SignalEval_Append(r.detail, "RSI OB/OS:OFF"); + else + { + rsiOk = (rsi[shift] < rsiOverbought); + if(rsiOk) + SignalEval_Append(r.detail, StringFormat("RSI:OK(%.1f<%g)", rsi[shift], rsiOverbought)); + else + { + SignalEval_Append(r.detail, StringFormat("RSI:OB %.1f>=%g", rsi[shift], rsiOverbought)); + SignalEval_SetFail(r.failTag, "RSI quá mua"); + } + } + + const bool envBar = (!sessionFilterEnabled) || sessionAtBar; + if(!sessionFilterEnabled) + SignalEval_Append(r.detail, "Phiên:OFF"); + else if(envBar) + SignalEval_Append(r.detail, "Phiên(bar):OK"); + else + { + SignalEval_Append(r.detail, StringLen(sessionFailWhy) > 0 + ? "Phiên(bar):FAIL " + sessionFailWhy + : "Phiên(bar):FAIL"); + SignalEval_SetFail(r.failTag, "Phiên"); + } + + r.signalOk = cross && coreOk && trendUp + && (!atrFilterEnabled || atrExpAtBar) && rsiOk && envBar; + r.tradeOk = r.signalOk; + + if(r.signalOk && r.tradeOk) + r.summary = "HỢP LỆ → vào lệnh"; + else if(r.signalOk) + r.summary = "Tín hiệu OK | chờ đặt lệnh"; + else + r.summary = StringLen(r.failTag) > 0 + ? "SKIP → " + r.failTag + : "SKIP"; + + return r; +} + +SignalEvalResult Signal_EvaluateSellAt(const int shift, const int rates_total, const int trendN, + const double &rsi[], const double &ema9[], const double &wma[], + const double &closeArr[], const double &ema200Arr[], + const PhaseEntryConfig &phaseCfg, + const bool phaseFilterEnabled, + const bool trendFilterEnabled, + const bool atrFilterEnabled, + const bool rsiObOsFilterEnabled, + const double rsiOverbought, + const double rsiOversold, + const bool sessionFilterEnabled, + const bool atrExpAtBar, + const bool sessionAtBar, + const string sessionFailWhy = "") +{ + SignalEvalResult r; + r.signalOk = false; + r.tradeOk = false; + r.summary = ""; + r.detail = ""; + r.failTag = ""; + + const bool cross = (shift + 1 < rates_total) && + (rsi[shift + 1] >= wma[shift + 1]) && (rsi[shift] < wma[shift + 1]); + if(!cross) + { + r.summary = "no cross"; + SignalEval_Append(r.detail, "RSI chưa cắt xuống WMA45"); + return r; + } + + SignalEval_Append(r.detail, "RSI↓WMA45"); + + bool coreOk = true; + string phaseFail = ""; + if(phaseFilterEnabled) + { + coreOk = Phase_SellPasses(shift, rates_total, rsi, ema9, wma, phaseCfg, phaseFail); + double maxSp = 0.0; + const bool p1 = Phase_FindMaxSpreadUp(shift, phaseCfg.expandLookback, rates_total, + rsi, ema9, wma, phaseCfg.minExpandSpread, maxSp); + const int xc = Phase_CountRsiEma9Crosses(shift, phaseCfg.coilLookback, rates_total, rsi, ema9); + const bool p3 = Phase_Ema9TurningSell(shift, phaseCfg, ema9); + const bool p4 = Phase_Wma45FlatteningSellCfg(shift, phaseCfg, wma); + const double gap = ema9[shift] - wma[shift]; + const bool p5 = Phase_Ema9NearWmaSell(shift, phaseCfg.maxEma9WmaGap, ema9, wma); + + SignalEval_Append(r.detail, p1 ? StringFormat("P1:OK(sp=%.1f)", maxSp) : "P1:FAIL"); + SignalEval_Append(r.detail, StringFormat("P2:%s(x=%d)", xc >= phaseCfg.minRsiEma9Crosses ? "OK" : "FAIL", xc)); + SignalEval_Append(r.detail, p3 ? "P3:OK" : "P3:FAIL"); + SignalEval_Append(r.detail, p4 ? "P4:OK" : "P4:FAIL"); + SignalEval_Append(r.detail, p5 ? StringFormat("P5:OK(gap=%.1f)", gap) : StringFormat("P5:FAIL(gap=%.1f)", gap)); + + if(!p1) SignalEval_SetFail(r.failTag, "P1-mở rộng"); + else if(xc < phaseCfg.minRsiEma9Crosses) SignalEval_SetFail(r.failTag, "P2-cuộn"); + else if(!p3) SignalEval_SetFail(r.failTag, "P3-EMA9↓"); + else if(!p4) SignalEval_SetFail(r.failTag, "P4-WMA45 phẳng"); + else if(!p5) SignalEval_SetFail(r.failTag, "P5-EMA9 xa WMA45"); + else if(!coreOk && StringLen(phaseFail) > 0) + SignalEval_SetFail(r.failTag, phaseFail); + } + else + { + coreOk = (ema9[shift] > wma[shift]); + SignalEval_Append(r.detail, coreOk ? "EMA9>WMA45" : "EMA9<=WMA45 FAIL"); + if(!coreOk) + SignalEval_SetFail(r.failTag, "EMA9<=WMA45"); + } + + bool trendDown = true; + if(!trendFilterEnabled) + SignalEval_Append(r.detail, "EMA200:OFF"); + else + { + for(int k = 0; k < trendN; k++) + { + const int idx = shift + k; + if(closeArr[idx] >= ema200Arr[idx]) + { + trendDown = false; + break; + } + } + SignalEval_Append(r.detail, trendDown ? "EMA200:OK" : "EMA200:FAIL"); + if(!trendDown) + SignalEval_SetFail(r.failTag, "EMA200"); + } + + SignalEval_Append(r.detail, !atrFilterEnabled ? "ATR:OFF" : (atrExpAtBar ? "ATR:OK" : "ATR:FAIL")); + if(atrFilterEnabled && !atrExpAtBar) + SignalEval_SetFail(r.failTag, "ATR co"); + + bool rsiOk = true; + if(!rsiObOsFilterEnabled) + SignalEval_Append(r.detail, "RSI OB/OS:OFF"); + else + { + rsiOk = (rsi[shift] > rsiOversold); + if(rsiOk) + SignalEval_Append(r.detail, StringFormat("RSI:OK(%.1f>%g)", rsi[shift], rsiOversold)); + else + { + SignalEval_Append(r.detail, StringFormat("RSI:OS %.1f<=%g", rsi[shift], rsiOversold)); + SignalEval_SetFail(r.failTag, "RSI quá bán"); + } + } + + const bool envBar = (!sessionFilterEnabled) || sessionAtBar; + if(!sessionFilterEnabled) + SignalEval_Append(r.detail, "Phiên:OFF"); + else if(envBar) + SignalEval_Append(r.detail, "Phiên(bar):OK"); + else + { + SignalEval_Append(r.detail, StringLen(sessionFailWhy) > 0 + ? "Phiên(bar):FAIL " + sessionFailWhy + : "Phiên(bar):FAIL"); + SignalEval_SetFail(r.failTag, "Phiên"); + } + + r.signalOk = cross && coreOk && trendDown && (!atrFilterEnabled || atrExpAtBar) && rsiOk && envBar; + r.tradeOk = r.signalOk; + + if(r.signalOk && r.tradeOk) + r.summary = "HỢP LỆ → vào lệnh"; + else if(r.signalOk) + r.summary = "Tín hiệu OK | chờ đặt lệnh"; + else + r.summary = StringLen(r.failTag) > 0 + ? "SKIP → " + r.failTag + : "SKIP"; + + return r; +} + +const string DBG_HOVER_LABEL = "RsiMomEA_dbg_hover"; + +void Signal_DebugSetTooltip(const long chartId, const string name, const string tip) +{ + ObjectSetString(chartId, name, OBJPROP_TOOLTIP, tip); +} + +void Signal_DebugEnsureHoverLabel(const long chartId) +{ + if(ObjectFind(chartId, DBG_HOVER_LABEL) >= 0) + return; + + ObjectCreate(chartId, DBG_HOVER_LABEL, OBJ_LABEL, 0, 0, 0); + ObjectSetInteger(chartId, DBG_HOVER_LABEL, OBJPROP_CORNER, CORNER_LEFT_LOWER); + ObjectSetInteger(chartId, DBG_HOVER_LABEL, OBJPROP_XDISTANCE, 8); + ObjectSetInteger(chartId, DBG_HOVER_LABEL, OBJPROP_YDISTANCE, 72); + ObjectSetInteger(chartId, DBG_HOVER_LABEL, OBJPROP_FONTSIZE, 8); + ObjectSetString (chartId, DBG_HOVER_LABEL, OBJPROP_FONT, "Consolas"); + ObjectSetInteger(chartId, DBG_HOVER_LABEL, OBJPROP_COLOR, clrWhite); + ObjectSetInteger(chartId, DBG_HOVER_LABEL, OBJPROP_BACK, true); + ObjectSetInteger(chartId, DBG_HOVER_LABEL, OBJPROP_BGCOLOR, clrBlack); + ObjectSetInteger(chartId, DBG_HOVER_LABEL, OBJPROP_SELECTABLE, false); + ObjectSetInteger(chartId, DBG_HOVER_LABEL, OBJPROP_HIDDEN, true); + ObjectSetString (chartId, DBG_HOVER_LABEL, OBJPROP_TEXT, ""); +} + +void Signal_DebugShowHoverHint(const long chartId, const string tip) +{ + if(StringLen(tip) == 0) + return; + Signal_DebugEnsureHoverLabel(chartId); + ObjectSetString(chartId, DBG_HOVER_LABEL, OBJPROP_TEXT, tip); + ChartRedraw(chartId); +} + +void Signal_DebugHideHoverHint(const long chartId) +{ + if(ObjectFind(chartId, DBG_HOVER_LABEL) < 0) + return; + const string cur = ObjectGetString(chartId, DBG_HOVER_LABEL, OBJPROP_TEXT); + if(StringLen(cur) == 0) + return; + ObjectSetString(chartId, DBG_HOVER_LABEL, OBJPROP_TEXT, ""); + ChartRedraw(chartId); +} + +bool Signal_DebugObjectHasPrefix(const string name, const string prefix) +{ + return (StringLen(name) >= StringLen(prefix) && StringSubstr(name, 0, StringLen(prefix)) == prefix); +} + +bool Signal_DebugReadTipFromMark(const long chartId, const string objName, string &tip) +{ + tip = ""; + if(ObjectFind(chartId, objName) < 0) + return false; + tip = ObjectGetString(chartId, objName, OBJPROP_TOOLTIP); + return (StringLen(tip) > 0); +} + +bool Signal_DebugFindTipAtMouse(const long chartId, const string prefix, + const int mouseX, const int mouseY, string &tip) +{ + tip = ""; + datetime tMouse = 0; + double pMouse = 0.0; + int subwin = 0; + if(!ChartXYToTimePrice(chartId, mouseX, mouseY, subwin, tMouse, pMouse)) + return false; + + const int total = ObjectsTotal(chartId, 0, -1); + for(int i = total - 1; i >= 0; i--) + { + const string name = ObjectName(chartId, i, 0, -1); + if(!Signal_DebugObjectHasPrefix(name, prefix)) + continue; + + const bool isHit = (StringFind(name, "_hit") >= 0); + const bool isArrow = (StringFind(name, "_ar") >= 0); + if(!isHit && !isArrow) + continue; + + if(isHit) + { + const datetime t0 = (datetime)ObjectGetInteger(chartId, name, OBJPROP_TIME, 0); + const datetime t1 = (datetime)ObjectGetInteger(chartId, name, OBJPROP_TIME, 1); + const double p0 = ObjectGetDouble(chartId, name, OBJPROP_PRICE, 0); + const double p1 = ObjectGetDouble(chartId, name, OBJPROP_PRICE, 1); + if(tMouse < t0 || tMouse > t1) + continue; + if(pMouse < MathMin(p0, p1) || pMouse > MathMax(p0, p1)) + continue; + return Signal_DebugReadTipFromMark(chartId, name, tip); + } + + if(isArrow) + { + const datetime ta = (datetime)ObjectGetInteger(chartId, name, OBJPROP_TIME, 0); + const int halfSec = MathMax(1, PeriodSeconds(_Period) / 2); + if(MathAbs((int)(tMouse - ta)) > halfSec) + continue; + return Signal_DebugReadTipFromMark(chartId, name, tip); + } + } + + const int sh = iBarShift(_Symbol, _Period, tMouse, false); + if(sh < 0) + return false; + + for(int d = -1; d <= 1; d++) + { + const int s = sh + d; + if(s < 0) + continue; + const datetime bt = iTime(_Symbol, _Period, s); + const string btKey = IntegerToString((int)bt); + const string tryN[4]; + tryN[0] = prefix + btKey + "_B_hit"; + tryN[1] = prefix + btKey + "_S_hit"; + tryN[2] = prefix + btKey + "_B_ar"; + tryN[3] = prefix + btKey + "_S_ar"; + for(int k = 0; k < 4; k++) + { + if(Signal_DebugReadTipFromMark(chartId, tryN[k], tip)) + return true; + } + } + return false; +} + +void Signal_DebugOnMouseMove(const long chartId, const string prefix, const int mouseX, const int mouseY) +{ + string tip = ""; + if(Signal_DebugFindTipAtMouse(chartId, prefix, mouseX, mouseY, tip)) + Signal_DebugShowHoverHint(chartId, tip); + else + Signal_DebugHideHoverHint(chartId); +} + +void Signal_DebugDrawMark(const long chartId, const string prefix, + const datetime barTime, const double barHigh, const double barLow, + const double markPrice, const bool isBuy, const SignalEvalResult &ev) +{ + const string base = prefix + IntegerToString((int)barTime) + (isBuy ? "_B" : "_S"); + const string arrowName = base + "_ar"; + const string hitName = base + "_hit"; + + // Giữ dấu cũ — không xóa/vẽ lại khi có nến mới (hover xem lý do skip sau) + if(ObjectFind(chartId, arrowName) >= 0) + return; + + const string tip = SignalEval_FormatTooltip(isBuy, ev); + + color clr = clrDimGray; + int arrowCode = 251; + int arrowW = 2; + if(ev.signalOk && ev.tradeOk) + { + clr = clrLime; + arrowCode = isBuy ? 233 : 234; + arrowW = 1; + } + else if(ev.signalOk) + { + clr = clrGold; + arrowCode = isBuy ? 233 : 234; + arrowW = 1; + } + else + { + clr = clrOrangeRed; + arrowCode = 251; + arrowW = 3; + } + + if(ObjectCreate(chartId, arrowName, OBJ_ARROW, 0, barTime, markPrice)) + { + ObjectSetInteger(chartId, arrowName, OBJPROP_ARROWCODE, arrowCode); + ObjectSetInteger(chartId, arrowName, OBJPROP_COLOR, clr); + ObjectSetInteger(chartId, arrowName, OBJPROP_WIDTH, arrowW); + ObjectSetInteger(chartId, arrowName, OBJPROP_ANCHOR, isBuy ? ANCHOR_TOP : ANCHOR_BOTTOM); + ObjectSetInteger(chartId, arrowName, OBJPROP_SELECTABLE, true); + ObjectSetInteger(chartId, arrowName, OBJPROP_HIDDEN, true); + ObjectSetInteger(chartId, arrowName, OBJPROP_BACK, false); + Signal_DebugSetTooltip(chartId, arrowName, tip); + } + + const datetime tEnd = barTime + (datetime)PeriodSeconds(_Period); + const double pad = MathMax(_Point * 8.0, (barHigh - barLow) * 0.08); + const double rHi = barHigh + pad; + const double rLo = barLow - pad; + const color hitFill = ColorToARGB(clr, (uchar)24); + + if(ObjectCreate(chartId, hitName, OBJ_RECTANGLE, 0, barTime, rHi, tEnd, rLo)) + { + ObjectSetInteger(chartId, hitName, OBJPROP_COLOR, hitFill); + ObjectSetInteger(chartId, hitName, OBJPROP_STYLE, STYLE_SOLID); + ObjectSetInteger(chartId, hitName, OBJPROP_WIDTH, 1); + ObjectSetInteger(chartId, hitName, OBJPROP_FILL, true); + ObjectSetInteger(chartId, hitName, OBJPROP_BACK, true); + ObjectSetInteger(chartId, hitName, OBJPROP_SELECTABLE, true); + ObjectSetInteger(chartId, hitName, OBJPROP_HIDDEN, true); + Signal_DebugSetTooltip(chartId, hitName, tip); + } +} + +void Signal_DebugConfigureChart(const long chartId) +{ + ChartSetInteger(chartId, CHART_SHOW_OBJECT_DESCR, false); + ChartSetInteger(chartId, CHART_EVENT_MOUSE_MOVE, true); + Signal_DebugEnsureHoverLabel(chartId); +} + +#endif diff --git a/Include/RsiMom/TradeJournal.mqh b/Include/RsiMom/TradeJournal.mqh new file mode 100644 index 0000000..cae8f9a --- /dev/null +++ b/Include/RsiMom/TradeJournal.mqh @@ -0,0 +1,411 @@ +//+------------------------------------------------------------------+ +//| TradeJournal.mqh — CSV lệnh đóng + tổng hợp tháng (RsiMomentumEA)| +//+------------------------------------------------------------------+ +#ifndef RSI_MOM_TRADE_JOURNAL_MQH +#define RSI_MOM_TRADE_JOURNAL_MQH + +struct RsiMomMonthRow +{ + string ym; + int trades; + int wins; + int losses; + double netProfit; + double grossProfit; + double grossLoss; + int slCount; + int tpCount; + int otherCount; +}; + +int g_rsiMomJournalHandle = INVALID_HANDLE; +RsiMomMonthRow g_rsiMomMonths[]; +int g_rsiMomMonthCount = 0; + +string RsiMomJournal_TradesPath(const string sym, const ENUM_TIMEFRAMES tf) +{ + return StringFormat("RsiMomEA\\journal_%s_%s.csv", sym, EnumToString(tf)); +} + +string RsiMomJournal_SummaryPath(const string sym, const ENUM_TIMEFRAMES tf) +{ + return StringFormat("RsiMomEA\\summary_%s_%s.csv", sym, EnumToString(tf)); +} + +int RsiMomJournal_FindMonth(const string ym) +{ + for(int i = 0; i < g_rsiMomMonthCount; i++) + { + if(g_rsiMomMonths[i].ym == ym) + return i; + } + return -1; +} + +void RsiMomJournal_AddMonth(const string ym, const double profit, const string exitTag) +{ + int idx = RsiMomJournal_FindMonth(ym); + if(idx < 0) + { + idx = g_rsiMomMonthCount; + g_rsiMomMonthCount++; + ArrayResize(g_rsiMomMonths, g_rsiMomMonthCount); + g_rsiMomMonths[idx].ym = ym; + g_rsiMomMonths[idx].trades = 0; + g_rsiMomMonths[idx].wins = 0; + g_rsiMomMonths[idx].losses = 0; + g_rsiMomMonths[idx].netProfit = 0.0; + g_rsiMomMonths[idx].grossProfit = 0.0; + g_rsiMomMonths[idx].grossLoss = 0.0; + g_rsiMomMonths[idx].slCount = 0; + g_rsiMomMonths[idx].tpCount = 0; + g_rsiMomMonths[idx].otherCount = 0; + } + + RsiMomMonthRow r = g_rsiMomMonths[idx]; + r.trades++; + r.netProfit += profit; + if(profit > 0.0) + { + r.wins++; + r.grossProfit += profit; + } + else if(profit < 0.0) + { + r.losses++; + r.grossLoss += profit; + } + + if(exitTag == "SL") + r.slCount++; + else if(exitTag == "TP") + r.tpCount++; + else + r.otherCount++; + + g_rsiMomMonths[idx] = r; +} + +bool RsiMomJournal_DeleteIfExists(const string path) +{ + if(!FileIsExist(path, FILE_COMMON)) + return false; + return FileDelete(path, FILE_COMMON); +} + +bool RsiMomJournal_ResetFiles(const string sym, const ENUM_TIMEFRAMES tf) +{ + RsiMomJournal_CloseFiles(); + RsiMomJournal_DeleteIfExists(RsiMomJournal_TradesPath(sym, tf)); + RsiMomJournal_DeleteIfExists(RsiMomJournal_SummaryPath(sym, tf)); + return true; +} + +bool RsiMomJournal_OpenTrades(const string sym, const ENUM_TIMEFRAMES tf) +{ + if(g_rsiMomJournalHandle != INVALID_HANDLE) + return true; + + FolderCreate("RsiMomEA", FILE_COMMON); + const string path = RsiMomJournal_TradesPath(sym, tf); + const bool exists = FileIsExist(path, FILE_COMMON); + + g_rsiMomJournalHandle = FileOpen(path, FILE_READ | FILE_WRITE | FILE_CSV | FILE_COMMON | FILE_ANSI, ','); + if(g_rsiMomJournalHandle == INVALID_HANDLE) + { + Print("[RsiMomEA] Journal: không mở file ", path, " err=", GetLastError()); + return false; + } + + if(!exists || FileSize(g_rsiMomJournalHandle) == 0) + { + FileWrite(g_rsiMomJournalHandle, + "close_time", "year_month", "profit", "dir", "exit", + "entry_time", "entry_price", "sl", "tp", "volume", + "risk_pts", "profit_r", + "rsi", "wma45", "ema200", "close_entry", "above_ema200", + "atr", "atr_ratio", "atr_exp_ok", + "session_ok", "spread_pts", + "symbol", "timeframe", "magic"); + } + else + { + FileSeek(g_rsiMomJournalHandle, 0, SEEK_END); + } + return true; +} + +void RsiMomJournal_CloseFiles() +{ + if(g_rsiMomJournalHandle != INVALID_HANDLE) + { + FileClose(g_rsiMomJournalHandle); + g_rsiMomJournalHandle = INVALID_HANDLE; + } +} + +bool RsiMomJournal_Copy1(const int handle, const int shift, double &v) +{ + v = 0.0; + if(handle == INVALID_HANDLE || shift < 0) + return false; + double buf[]; + ArraySetAsSeries(buf, true); + if(CopyBuffer(handle, 0, shift, 1, buf) < 1) + return false; + v = buf[0]; + return true; +} + +bool RsiMomJournal_AtrRatioAt(const int hAtr, const int shift, + const int cmpBars, const double minRatio, + double &atrNow, double &ratio, bool &expOk) +{ + atrNow = 0.0; + ratio = 0.0; + expOk = false; + double atrRef = 0.0; + if(!RsiMomJournal_Copy1(hAtr, shift, atrNow) || !RsiMomJournal_Copy1(hAtr, shift + cmpBars, atrRef)) + return false; + if(atrRef <= 0.0) + return false; + ratio = atrNow / atrRef; + expOk = (ratio >= MathMax(1.0, minRatio) - 1e-8); + return true; +} + +string RsiMomJournal_ExitTag(const long dealReason, const double profit) +{ + if(dealReason == DEAL_REASON_SL) + return "SL"; + if(dealReason == DEAL_REASON_TP) + return "TP"; + if(profit >= 0.0) + return "WIN"; + return "LOSS"; +} + +bool RsiMomJournal_FindEntryDeal(const ulong positionId, + ulong &entryDeal, + datetime &entryTime, + long &posType, + double &entryPrice, + double &sl, + double &tp, + double &volume) +{ + entryDeal = 0; + entryTime = 0; + posType = 0; + entryPrice = sl = tp = volume = 0.0; + + if(positionId == 0 || !HistorySelectByPosition(positionId)) + return false; + + const int total = HistoryDealsTotal(); + for(int i = 0; i < total; i++) + { + const ulong d = HistoryDealGetTicket(i); + if(d == 0 || !HistoryDealSelect(d)) + continue; + if(HistoryDealGetInteger(d, DEAL_ENTRY) != DEAL_ENTRY_IN) + continue; + + entryDeal = d; + entryTime = (datetime)HistoryDealGetInteger(d, DEAL_TIME); + posType = HistoryDealGetInteger(d, DEAL_TYPE); + entryPrice = HistoryDealGetDouble(d, DEAL_PRICE); + volume = HistoryDealGetDouble(d, DEAL_VOLUME); + + const ulong orderTicket = (ulong)HistoryDealGetInteger(d, DEAL_ORDER); + if(orderTicket > 0 && HistoryOrderSelect(orderTicket)) + { + sl = HistoryOrderGetDouble(orderTicket, ORDER_SL); + tp = HistoryOrderGetDouble(orderTicket, ORDER_TP); + } + return true; + } + return false; +} + +void RsiMomJournal_WriteSummary(const string sym, const ENUM_TIMEFRAMES tf) +{ + if(g_rsiMomMonthCount <= 0) + return; + + const string path = RsiMomJournal_SummaryPath(sym, tf); + const int h = FileOpen(path, FILE_WRITE | FILE_CSV | FILE_COMMON | FILE_ANSI, ','); + if(h == INVALID_HANDLE) + { + Print("[RsiMomEA] Summary CSV lỗi ", path, " err=", GetLastError()); + return; + } + + FileWrite(h, + "year_month", "trades", "wins", "losses", "winrate_pct", + "net_profit", "gross_profit", "gross_loss", "avg_profit", + "sl_count", "tp_count", "other_count"); + + for(int i = 0; i < g_rsiMomMonthCount; i++) + { + const RsiMomMonthRow r = g_rsiMomMonths[i]; + double winrate = 0.0; + if(r.trades > 0) + winrate = 100.0 * (double)r.wins / (double)r.trades; + const double avg = (r.trades > 0) ? r.netProfit / (double)r.trades : 0.0; + + FileWrite(h, + r.ym, + IntegerToString(r.trades), + IntegerToString(r.wins), + IntegerToString(r.losses), + DoubleToString(winrate, 2), + DoubleToString(r.netProfit, 2), + DoubleToString(r.grossProfit, 2), + DoubleToString(r.grossLoss, 2), + DoubleToString(avg, 2), + IntegerToString(r.slCount), + IntegerToString(r.tpCount), + IntegerToString(r.otherCount)); + } + + FileClose(h); + Print("[RsiMomEA] Summary CSV → ", path, " (FILE_COMMON, ", g_rsiMomMonthCount, " tháng)"); +} + +void RsiMomJournal_ResetMonths() +{ + ArrayResize(g_rsiMomMonths, 0); + g_rsiMomMonthCount = 0; +} + +void RsiMomJournal_OnDeinit(const string sym, const ENUM_TIMEFRAMES tf) +{ + RsiMomJournal_WriteSummary(sym, tf); + RsiMomJournal_CloseFiles(); +} + +void RsiMomJournal_RecordClosedDeal(const ulong dealOut, + const string sym, + const ENUM_TIMEFRAMES tf, + const ulong magic, + const int hRsi, + const int hWma45, + const int hEma200, + const int hAtrRegime, + const int atrCmpBars, + const double atrMinRatio, + const int sessionOkAtEntry) +{ + if(dealOut == 0 || !HistoryDealSelect(dealOut)) + return; + if(HistoryDealGetString(dealOut, DEAL_SYMBOL) != sym) + return; + if((ulong)HistoryDealGetInteger(dealOut, DEAL_MAGIC) != magic) + return; + if(HistoryDealGetInteger(dealOut, DEAL_ENTRY) != DEAL_ENTRY_OUT) + return; + + if(!RsiMomJournal_OpenTrades(sym, tf)) + return; + + const ulong posId = (ulong)HistoryDealGetInteger(dealOut, DEAL_POSITION_ID); + ulong entryDeal = 0; + datetime entryTime = 0; + long posType = 0; + double entryPrice = 0.0, sl = 0.0, tp = 0.0, volume = 0.0; + if(!RsiMomJournal_FindEntryDeal(posId, entryDeal, entryTime, posType, + entryPrice, sl, tp, volume)) + { + Print("[RsiMomEA] Journal: không tìm entry pos=", posId); + return; + } + + const int entryShift = iBarShift(sym, tf, entryTime, true); + if(entryShift < 1) + { + Print("[RsiMomEA] Journal: entry shift invalid ", TimeToString(entryTime)); + return; + } + + const int dig = (int)SymbolInfoInteger(sym, SYMBOL_DIGITS); + double rsi = 0.0, wma = 0.0, ema200 = 0.0, atrNow = 0.0, atrRatio = 0.0; + bool atrExpOk = false; + RsiMomJournal_Copy1(hRsi, entryShift, rsi); + RsiMomJournal_Copy1(hWma45, entryShift, wma); + RsiMomJournal_Copy1(hEma200, entryShift, ema200); + RsiMomJournal_AtrRatioAt(hAtrRegime, entryShift, atrCmpBars, atrMinRatio, + atrNow, atrRatio, atrExpOk); + + const double closeEntry = iClose(sym, tf, entryShift); + const int aboveEma = (ema200 > 0.0 && closeEntry > ema200) ? 1 : 0; + + const int sessionOk = (sessionOkAtEntry != 0) ? 1 : 0; + const int spreadPts = (int)SymbolInfoInteger(sym, SYMBOL_SPREAD); + + double riskPts = 0.0; + if(posType == DEAL_TYPE_BUY && sl > 0.0) + riskPts = (entryPrice - sl) / _Point; + else if(posType == DEAL_TYPE_SELL && sl > 0.0) + riskPts = (sl - entryPrice) / _Point; + if(riskPts < 0.0) + riskPts = 0.0; + + const datetime closeTime = (datetime)HistoryDealGetInteger(dealOut, DEAL_TIME); + MqlDateTime dt; + TimeToStruct(closeTime, dt); + const string yearMonth = StringFormat("%04d-%02d", dt.year, dt.mon); + + const double profit = HistoryDealGetDouble(dealOut, DEAL_PROFIT) + + HistoryDealGetDouble(dealOut, DEAL_SWAP) + + HistoryDealGetDouble(dealOut, DEAL_COMMISSION); + const long dealReason = HistoryDealGetInteger(dealOut, DEAL_REASON); + const string exitTag = RsiMomJournal_ExitTag(dealReason, profit); + const string dir = (posType == DEAL_TYPE_BUY) ? "BUY" : "SELL"; + + double profitR = 0.0; + if(riskPts > 0.0) + { + const double tickVal = SymbolInfoDouble(sym, SYMBOL_TRADE_TICK_VALUE); + const double tickSz = SymbolInfoDouble(sym, SYMBOL_TRADE_TICK_SIZE); + if(tickSz > 0.0 && tickVal > 0.0) + { + const double riskMoney = (riskPts * _Point / tickSz) * tickVal * volume; + if(riskMoney > 0.0) + profitR = profit / riskMoney; + } + } + + RsiMomJournal_AddMonth(yearMonth, profit, exitTag); + + FileWrite(g_rsiMomJournalHandle, + TimeToString(closeTime, TIME_DATE | TIME_MINUTES), + yearMonth, + DoubleToString(profit, 2), + dir, + exitTag, + TimeToString(entryTime, TIME_DATE | TIME_MINUTES), + DoubleToString(entryPrice, dig), + DoubleToString(sl, dig), + DoubleToString(tp, dig), + DoubleToString(volume, 2), + DoubleToString(riskPts, 1), + DoubleToString(profitR, 3), + DoubleToString(rsi, 2), + DoubleToString(wma, dig), + DoubleToString(ema200, dig), + DoubleToString(closeEntry, dig), + IntegerToString(aboveEma), + DoubleToString(atrNow, dig), + DoubleToString(atrRatio, 4), + IntegerToString(atrExpOk ? 1 : 0), + IntegerToString(sessionOk), + IntegerToString(spreadPts), + sym, + EnumToString(tf), + IntegerToString((int)magic)); + + FileFlush(g_rsiMomJournalHandle); +} + +#endif