Condition chuẩn

This commit is contained in:
Bell
2026-05-23 22:27:48 +07:00
parent 1f1fc5b7cb
commit 71677f804b
4 changed files with 1647 additions and 0 deletions
+428
View File
@@ -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``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 P1P5 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 817).
- New York: `InpNYStartHour``InpNYEndHour` (1322).
- 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ổ 78h và 2122h (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 # P1P5
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 + P1P5** (+ 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. P1P2 giữ nguyên; P3P5 đã nới lỏng mặc định.*
+266
View File
@@ -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
+542
View File
@@ -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 <RsiMom/PhaseEntry.mqh>
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" : "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
+411
View File
@@ -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