Delete RSIForceStateEA

This commit is contained in:
Bell
2026-05-10 22:31:01 +07:00
parent d6d36fc3a3
commit 83f004afb3
8 changed files with 0 additions and 1928 deletions
-159
View File
@@ -1,159 +0,0 @@
//+------------------------------------------------------------------+
//| RSIForceStateEA - pullback by RSI/EMA9/WMA45 force, EMA200 trend |
//| State machine: NO_TRADE -> WATCHING -> PENDING_ORDER -> IN_TRADE |
//+------------------------------------------------------------------+
#property copyright "RSI Force State EA"
#property version "1.30"
#property strict
// ---- Layer 1: inputs + value types + indicator buffers/handles ----
#include "RSIForceStateEA/Config.mqh"
#include "RSIForceStateEA/State.mqh"
#include "RSIForceStateEA/Indicators.mqh"
// ---- Globals consumed by Layer 2/3 modules ----
EAState g_State = STATE_NO_TRADE;
PendingContext g_Pending;
TradeContext g_OpenTrade;
// ---- Layer 2: trade ops + state machine (depend on globals above) ----
#include "RSIForceStateEA/Trade.mqh"
#include "RSIForceStateEA/StateMachine.mqh"
// ---- Layer 3: visualization (depends on globals + state machine) ----
#include "RSIForceStateEA/Visualizer.mqh"
// Throttle stats panel refresh (deal history scan can be heavy).
datetime g_LastVisualRefresh = 0;
const int kVisualRefreshSeconds = 2;
// ------------------------------------------------------------------
// Input validation (fail fast on misconfiguration)
// ------------------------------------------------------------------
bool ValidateInputs()
{
if (InpRiskPercent <= 0.0 || InpRiskPercent > 50.0)
{ Print("[INIT] InpRiskPercent must be in (0, 50]"); return false; }
if (InpRiskRewardRatio <= 0.0)
{ Print("[INIT] InpRiskRewardRatio must be > 0"); return false; }
if (InpPartialCloseAtR <= 0.0 || InpPartialCloseAtR >= InpRiskRewardRatio)
{ Print("[INIT] InpPartialCloseAtR must be in (0, InpRiskRewardRatio)"); return false; }
if (InpPartialClosePercent <= 0.0 || InpPartialClosePercent >= 100.0)
{ Print("[INIT] InpPartialClosePercent must be in (0, 100)"); return false; }
if (InpPendingMaxAliveBars < 1)
{ Print("[INIT] InpPendingMaxAliveBars must be >= 1"); return false; }
if (InpWatchingMaxBars < 1)
{ Print("[INIT] InpWatchingMaxBars must be >= 1"); return false; }
if (InpSlopeLookbackBars < 2)
{ Print("[INIT] InpSlopeLookbackBars must be >= 2"); return false; }
if (InpMinBarsBetweenCrosses < 1)
{ Print("[INIT] InpMinBarsBetweenCrosses must be >= 1"); return false; }
if (InpSwingLookbackBars < 5)
{ Print("[INIT] InpSwingLookbackBars must be >= 5"); return false; }
if (InpSidewayRSILow >= InpSidewayRSIHigh)
{ Print("[INIT] InpSidewayRSILow must be < InpSidewayRSIHigh"); return false; }
if (InpRSI_EMA9Period >= InpRSI_WMA45Period)
{ Print("[INIT] InpRSI_EMA9Period must be < InpRSI_WMA45Period"); return false; }
if (InpSignalBarShift < 1)
{ Print("[INIT] InpSignalBarShift must be >= 1 (use closed bars)"); return false; }
return true;
}
// ------------------------------------------------------------------
// Lifecycle
// ------------------------------------------------------------------
int OnInit()
{
if (!ValidateInputs()) return INIT_PARAMETERS_INCORRECT;
ZeroMemory(g_Pending);
ZeroMemory(g_OpenTrade);
g_State = STATE_NO_TRADE;
g_HasTradedThisPullback = false;
g_BarsInWatching = 0;
g_LastTrend = TREND_NONE;
g_LastDirTrend = TREND_NONE;
g_LastVisualRefresh = 0;
if (!InitIndicators())
{
Print("[INIT] InitIndicators failed");
return INIT_FAILED;
}
if (!RefreshIndicatorData())
{
Print("[INIT] RefreshIndicatorData failed (need more history)");
return INIT_FAILED;
}
InitTradeOps();
// Seed both trend trackers to current value so we don't fire a false
// "direction flip" event on the first tick.
g_LastTrend = DetectTrend(InpSignalBarShift);
if (g_LastTrend != TREND_NONE) g_LastDirTrend = g_LastTrend;
PrintFormat("[INIT] RSIForceStateEA v1.30 ready | Symbol=%s | TF=%d | startTrend=%s",
_Symbol, (int)_Period, EnumToString(g_LastTrend));
AttachIndicatorsToChart();
DrawAllVisuals(g_LastTrend);
return INIT_SUCCEEDED;
}
void OnDeinit(const int reason)
{
RemoveAllVisuals();
ReleaseIndicators();
PrintFormat("[DEINIT] reason=%d", reason);
}
// ------------------------------------------------------------------
// Tick loop:
// - tick-level: refresh data, sync broker state, manage open trade
// - bar-level: evaluate state machine on every newly closed bar
// - visuals: refresh dashboard/levels (throttled)
// ------------------------------------------------------------------
void OnTick()
{
if (!RefreshIndicatorData()) return;
SyncStateWithBroker();
if (g_State == STATE_IN_TRADE)
ManagePartialAndBreakEven(g_OpenTrade);
const bool newBar = IsNewBar();
if (newBar) RunStateMachine();
// Throttle the dashboard refresh to avoid excessive history queries.
const datetime now = TimeCurrent();
if (newBar || (now - g_LastVisualRefresh) >= kVisualRefreshSeconds)
{
g_LastVisualRefresh = now;
DrawAllVisuals(DetectTrend(InpSignalBarShift));
}
}
// Trade events can change order/position state between ticks; just resync.
// Visuals will refresh on the next tick (throttled by kVisualRefreshSeconds).
void OnTradeTransaction(const MqlTradeTransaction &trans,
const MqlTradeRequest &request,
const MqlTradeResult &result)
{
SyncStateWithBroker();
}
-74
View File
@@ -1,74 +0,0 @@
#ifndef RSI_FORCE_STATE_EA__CONFIG_MQH
#define RSI_FORCE_STATE_EA__CONFIG_MQH
// ============================================================
// All EA inputs grouped by responsibility for clarity in MT5 UI
// ============================================================
input group "General"
input long InpMagicNumber = 26050901;
input bool InpDebugLog = true;
input group "Trend Filter (EMA200 on Close)"
input int InpEMA200Period = 200;
input bool InpUseATRTrendBuffer = false; // false = % buffer, true = ATR buffer
input double InpTrendBufferPercent = 0.10; // % EMA200 dead-zone (when ATR buffer off)
input double InpTrendBufferATRMult = 0.20; // ATR multiplier for dead-zone (when ATR buffer on)
input bool InpSkipFlatEMA200 = true;
input double InpFlatEMA_ATRMult = 0.05; // EMA200 considered flat if |delta| < ATR*this
input group "ATR (used by trend buffer + SL)"
input int InpATRPeriod = 14;
input group "Force Indicators (RSI + 2 MAs computed on RSI)"
input int InpRSIPeriod = 14;
input int InpRSI_EMA9Period = 9; // EMA tinh tren RSI
input int InpRSI_WMA45Period = 45; // WMA tinh tren RSI
input int InpSlopeLookbackBars = 3; // bars de check slope dong nhat
input int InpMinBarsBetweenCrosses = 10; // toi thieu N bar khong cross truoc khi nhan tin hieu
input group "Sideway / No-Trade Filters"
input bool InpUseRSISidewayFilter = true;
input int InpSidewayLookbackBars = 8;
input double InpSidewayRSILow = 45.0;
input double InpSidewayRSIHigh = 55.0;
input group "Entry & Pending Order"
input int InpSignalBarShift = 1; // 1 = nen vua dong (khuyen nghi)
input int InpPendingMaxAliveBars = 5; // huy pending sau N bar khong khop
input int InpWatchingMaxBars = 10; // huy WATCHING neu khong co trigger sau N bar
input bool InpInvalidateIfCrossBack = true; // huy pending khi RSI cross nguoc lai
input group "Stop Loss"
enum ENUM_SL_MODE
{
SL_SWING = 0, // theo swing extreme gan nhat
SL_ATR = 1, // theo ATR
SL_HYBRID = 2 // chon SL rong hon giua swing va ATR
};
input ENUM_SL_MODE InpStopLossMode = SL_HYBRID;
input int InpSwingLookbackBars = 20;
input double InpSL_ATRMult = 1.2;
input int InpSL_SwingBufferPoints = 20; // them buffer ngoai swing extreme
input group "Risk & Trade Management"
input double InpRiskPercent = 1.0;
input double InpRiskRewardRatio = 2.0; // TP = entry +/- R*RR
input double InpPartialCloseAtR = 1.5; // dong 1 phan khi dat R nay
input double InpPartialClosePercent = 50.0; // phan tram dong khi dat partial R
input group "Visualization"
input bool InpVisualize = true; // bat tat toan bo overlay
input bool InpAttachIndicators = true; // tu add EMA200 vao chart + RSI/EMA9/WMA45 vao subwindow
input bool InpShowDashboard = true; // panel goc tren-trai
input bool InpShowStatsPanel = true; // panel goc duoi-trai
input bool InpShowTradeLevels = true; // ve Entry/SL/TP TradingView style
input int InpStatsLookbackDays = 60; // chi quet history N ngay gan nhat cho stats
input color InpColorTrendUp = clrLime;
input color InpColorTrendDown = clrTomato;
input color InpColorTrendNone = clrSilver;
input color InpColorEntry = clrDodgerBlue;
input color InpColorSL = clrTomato;
input color InpColorTP = clrLime;
#endif
-122
View File
@@ -1,122 +0,0 @@
#ifndef RSI_FORCE_STATE_EA__INDICATORS_MQH
#define RSI_FORCE_STATE_EA__INDICATORS_MQH
// ============================================================
// Indicator handles + cached series buffers.
// All buffers are timeseries indexed (index 0 = current bar).
// ============================================================
int g_hRSI = INVALID_HANDLE;
int g_hEMA9 = INVALID_HANDLE; // EMA(InpRSI_EMA9Period) computed on RSI
int g_hWMA45 = INVALID_HANDLE; // WMA(InpRSI_WMA45Period) computed on RSI
int g_hEMA200 = INVALID_HANDLE; // EMA(InpEMA200Period) on close
int g_hATR = INVALID_HANDLE;
// Dynamic arrays so ArraySetAsSeries(...) is allowed.
double g_RSI[];
double g_EMA9[];
double g_WMA45[];
double g_EMA200[];
double g_ATR[];
MqlRates g_Bars[];
// How many bars we keep cached. Must be > swing/sideway/cross lookback.
const int kIndicatorCacheBars = 200;
// ------------------------------------------------------------
// Lifecycle
// ------------------------------------------------------------
bool InitIndicators()
{
g_hRSI = iRSI(_Symbol, _Period, InpRSIPeriod, PRICE_CLOSE);
if (g_hRSI == INVALID_HANDLE) return false;
// EMA9 / WMA45 are computed on the RSI buffer (not on price).
g_hEMA9 = iMA(_Symbol, _Period, InpRSI_EMA9Period, 0, MODE_EMA, g_hRSI);
if (g_hEMA9 == INVALID_HANDLE) return false;
g_hWMA45 = iMA(_Symbol, _Period, InpRSI_WMA45Period, 0, MODE_LWMA, g_hRSI);
if (g_hWMA45 == INVALID_HANDLE) return false;
g_hEMA200 = iMA(_Symbol, _Period, InpEMA200Period, 0, MODE_EMA, PRICE_CLOSE);
if (g_hEMA200 == INVALID_HANDLE) return false;
g_hATR = iATR(_Symbol, _Period, InpATRPeriod);
if (g_hATR == INVALID_HANDLE) return false;
ArraySetAsSeries(g_RSI, true);
ArraySetAsSeries(g_EMA9, true);
ArraySetAsSeries(g_WMA45, true);
ArraySetAsSeries(g_EMA200, true);
ArraySetAsSeries(g_ATR, true);
ArraySetAsSeries(g_Bars, true);
return true;
}
void ReleaseIndicators()
{
if (g_hRSI != INVALID_HANDLE) IndicatorRelease(g_hRSI);
if (g_hEMA9 != INVALID_HANDLE) IndicatorRelease(g_hEMA9);
if (g_hWMA45 != INVALID_HANDLE) IndicatorRelease(g_hWMA45);
if (g_hEMA200 != INVALID_HANDLE) IndicatorRelease(g_hEMA200);
if (g_hATR != INVALID_HANDLE) IndicatorRelease(g_hATR);
}
// Refresh the cached series buffers; returns false on partial copy.
bool RefreshIndicatorData()
{
const int n = kIndicatorCacheBars;
if (CopyRates(_Symbol, _Period, 0, n, g_Bars) < n) return false;
if (CopyBuffer(g_hRSI, 0, 0, n, g_RSI) < n) return false;
if (CopyBuffer(g_hEMA9, 0, 0, n, g_EMA9) < n) return false;
if (CopyBuffer(g_hWMA45, 0, 0, n, g_WMA45) < n) return false;
if (CopyBuffer(g_hEMA200, 0, 0, n, g_EMA200) < n) return false;
if (CopyBuffer(g_hATR, 0, 0, n, g_ATR) < n) return false;
return true;
}
// ------------------------------------------------------------
// Helpers used by the state machine on series-indexed buffers
// ------------------------------------------------------------
// True when buffer is monotonically decreasing across [shift .. shift+lookback-1].
// In timeseries indexing: newer bar = lower index, so "down slope" means
// older value > newer value, i.e. buffer[i+1] > buffer[i].
bool IsBufferSlopingDown(const double &buffer[], const int shift, const int lookback)
{
for (int i = shift; i < shift + lookback - 1; i++)
{
if (!(buffer[i] < buffer[i + 1])) return false;
}
return true;
}
// True when buffer is monotonically increasing across [shift .. shift+lookback-1].
bool IsBufferSlopingUp(const double &buffer[], const int shift, const int lookback)
{
for (int i = shift; i < shift + lookback - 1; i++)
{
if (!(buffer[i] > buffer[i + 1])) return false;
}
return true;
}
// Returns true if (a-b) sign changes anywhere within
// [fromShift .. fromShift+lookback-1]. Used to ensure RSI has stayed
// on one side of WMA45 for at least N bars before a fresh trigger.
bool HasCrossInLastNBars(const double &a[], const double &b[], const int fromShift, const int lookback)
{
for (int i = fromShift; i < fromShift + lookback; i++)
{
const double diffNew = a[i] - b[i];
const double diffOld = a[i + 1] - b[i + 1];
if (diffNew == 0.0 || diffOld == 0.0
|| (diffNew > 0.0 && diffOld < 0.0)
|| (diffNew < 0.0 && diffOld > 0.0))
return true;
}
return false;
}
#endif
-456
View File
@@ -1,456 +0,0 @@
# RSIForceStateEA
EA giao dịch pullback theo động lực RSI, lọc xu hướng bằng EMA200, điều phối bằng state
machine 4 trạng thái. Mỗi pullback chỉ một lệnh, vào bằng **BUY/SELL LIMIT** (không đuổi giá market).
## 1) Cấu trúc thư mục
- EA chính: `Experts/RSIForceStateEA.mq5`
- Các module:
- `Experts/RSIForceStateEA/Config.mqh` — toàn bộ tham số đầu vào (`input`)
- `Experts/RSIForceStateEA/State.mqh``enum` + `struct` (không chứa biến global)
- `Experts/RSIForceStateEA/Indicators.mqh` — handle chỉ báo + buffer + hàm slope/cross
- `Experts/RSIForceStateEA/Trade.mqh` — đặt lệnh, khối lượng theo rủi ro, partial + BE
- `Experts/RSIForceStateEA/StateMachine.mqh` — luồng 4 trạng thái
- `Experts/RSIForceStateEA/Visualizer.mqh` — dashboard, bảng thống kê, mức Entry/SL/TP
- `Experts/RSIForceStateEA/README.md` — tài liệu này
Thứ tự `#include` trong file `.mq5`:
```
Layer 1 : Config -> State -> Indicators
Globals : g_State, g_Pending, g_OpenTrade
Layer 2 : Trade -> StateMachine
Layer 3 : Visualizer
```
## 2) Logic tổng thể
### 2.1 Các chỉ báo
- `RSI(InpRSIPeriod)` — động lực giá.
- `EMA(InpRSI_EMA9Period)` — tính **trên** chuỗi RSI.
- `WMA(InpRSI_WMA45Period)` — tính **trên** chuỗi RSI.
- `EMA(InpEMA200Period)` — trên **giá đóng** — lọc xu hướng.
- `ATR(InpATRPeriod)` — dùng cho vùng đệm trend và SL.
### 2.2 Bộ lọc xu hướng (trend)
- **Uptrend** khi `Close > EMA200 + buffer`.
- **Downtrend** khi `Close < EMA200 - buffer`.
- **Buffer**:
- Theo `%` EMA200 mặc định (`InpTrendBufferPercent = 0.10%`), hoặc
- Theo ATR nếu `InpUseATRTrendBuffer = true`.
- `InpSkipFlatEMA200`: bỏ qua khi EMA200 “phẳng” (chênh lệch giữa hai nến ≤ `ATR * InpFlatEMA_ATRMult`).
### 2.3 Bốn trạng thái
| Trạng thái | Ý nghĩa |
| ----------- | ------- |
| `STATE_NO_TRADE` | Chưa có setup hợp lệ |
| `STATE_WATCHING` | Đã có pullback hợp lệ, đang chờ tín hiệu vào lệnh |
| `STATE_PENDING_ORDER` | Đã đặt BUY/SELL LIMIT, chờ khớp |
| `STATE_IN_TRADE` | Đã khớp, đang quản lý vị thế (partial + BE + TP) |
### 2.4 Pullback (`NO_TRADE` → `WATCHING`)
- Pullback trong **uptrend**:
- `RSI < EMA9 < WMA45`
- Cả ba đường cùng **slope giảm** (kiểm tra trên `InpSlopeLookbackBars` nến).
- Pullback trong **downtrend**: đối xứng (slope tăng).
### 2.5 Kích hoạt vào lệnh (`WATCHING` → `PENDING_ORDER`)
- **Tín hiệu BUY**:
- RSI cắt **lên** WMA45 (so sánh nến `InpSignalBarShift` với nến trước).
- EMA9 vẫn **dưới** WMA45.
- Trong `InpMinBarsBetweenCrosses` nến trước đó **không** có cắt RSI/WMA45 (tín hiệu “cô lập”, tránh nhiễu sát WMA45).
- **Tín hiệu SELL**: đối xứng.
### 2.6 Đặt lệnh LIMIT
- `Entry = (Close(nến tín hiệu) + SwingExtreme) / 2`
- **BUY**: SwingExtreme = đáy gần nhất trong `InpSwingLookbackBars`.
- **SELL**: SwingExtreme = đỉnh gần nhất.
- Lệnh: `BuyLimit` / `SellLimit` (không vào market).
- Pending tối đa `InpPendingMaxAliveBars` nến; hết hạn → hủy.
- Nếu `InpInvalidateIfCrossBack = true`: RSI cắt ngược lại WMA45 hoặc mất trend → hủy.
- `WATCHING` tối đa `InpWatchingMaxBars` nến không có trigger → về `NO_TRADE`.
### 2.7 Stop Loss
| Chế độ | Công thức |
| ------ | --------- |
| `SL_SWING` | Cực trị swing ± `InpSL_SwingBufferPoints` (point) |
| `SL_ATR` | `Entry ± ATR * InpSL_ATRMult` |
| `SL_HYBRID` | Chọn SL **rộng hơn** giữa swing và ATR (mặc định, an toàn hơn) |
### 2.8 Take Profit
- `TP = Entry ± R * InpRiskRewardRatio` (mặc định `2R`).
### 2.9 Quản lý khi `IN_TRADE`
- Rủi ro: `InpRiskPercent` (% số dư, mặc định 1%).
- Khi giá đạt `InpPartialCloseAtR` lần **R thực tế** (mặc định `1.5R`):
- Đóng `InpPartialClosePercent` % khối lượng (mặc định 50%).
- Dời SL về **giá khớp** (BE).
- Nếu không chia được lot (bị `volume min` chặn): vẫn dời SL về BE, bỏ qua partial.
- Không có hành động đặc biệt tại `1R`.
### 2.10 Chống spam (một pullback — một lệnh thử)
- Biến `g_HasTradedThisPullback`: sau khi **đặt thành công** pending cho pullback hiện tại → bật.
- **Reset** `g_HasTradedThisPullback` khi:
- Pending bị hủy / hết hạn / invalid (chưa khớp → được thử pullback khác).
- Vị thế đã đóng hoàn toàn (TP/SL/đóng tay).
- **Đảo chiều xu hướng có hướng** UP ↔ DOWN (đi qua `TREND_NONE` ngắn **không** coi là đảo chiều).
- Khi đảo chiều UP ↔ DOWN mà còn pending ngược hướng → hủy pending.
### 2.11 Bộ lọc RSI sideway
- `InpUseRSISidewayFilter`: nếu **tất cả** `InpSidewayLookbackBars` giá trị RSI gần nhất nằm trong `[InpSidewayRSILow, InpSidewayRSIHigh]` → bỏ qua tìm setup mới.
- Bộ lọc này **chỉ** chặn nhánh `NO_TRADE → WATCHING`; **không** chặn vòng đời pending và **không** chặn quản lý vị thế.
## 3) Cách biên dịch và chạy
1. Mở MetaEditor.
2. Mở `MQL5/Experts/RSIForceStateEA.mq5` và biên dịch (F7).
3. Quay lại MT5, gắn EA vào biểu đồ cần giao dịch.
4. Bật **Algo Trading**.
5. Có thể chỉnh `Inputs` ngay trên giao diện khi gắn EA.
## 4) Các tham số quan trọng (mặc định)
| Tham số | Mặc định | Mô tả |
| ------- | -------- | ----- |
| `InpMagicNumber` | 26050901 | Magic; đổi nếu chạy nhiều bản EA |
| `InpRiskPercent` | 1.0 | % số dư / mỗi lệnh |
| `InpRiskRewardRatio` | 2.0 | TP = R × hệ số này |
| `InpPartialCloseAtR` | 1.5 | Chốt một phần khi đạt R này (phải < RR) |
| `InpPartialClosePercent` | 50.0 | % khối lượng chốt partial |
| `InpPendingMaxAliveBars` | 5 | Hủy pending sau N nến |
| `InpWatchingMaxBars` | 10 | Thoát WATCHING nếu không có trigger |
| `InpMinBarsBetweenCrosses` | 10 | Khoảng cách tối thiểu giữa các lần cắt RSI/WMA45 |
| `InpSlopeLookbackBars` | 3 | Số nến kiểm tra slope |
| `InpSwingLookbackBars` | 20 | Cửa sổ tìm swing cho entry/SL |
| `InpStopLossMode` | SL_HYBRID | `SL_SWING` / `SL_ATR` / `SL_HYBRID` |
| `InpSL_ATRMult` | 1.2 | Hệ số ATR cho SL kiểu ATR |
| `InpSL_SwingBufferPoints` | 20 | Đệm thêm (point) ngoài cực trị swing |
| `InpUseRSISidewayFilter` | true | Bật/tắt lọc sideway RSI |
| `InpSidewayRSILow / High` | 45 / 55 | Dải sideway theo RSI |
| `InpUseATRTrendBuffer` | false | Dùng buffer trend theo ATR |
## 5) Gợi ý kiểm thử và tối ưu
- Nên backtest từng symbol ít nhất 612 tháng trước khi chạy thật.
- Thị trường biến động mạnh: thử `InpUseATRTrendBuffer = true`.
- Muốn **ít tín hiệu hơn nhưng chọn lọc hơn**:
- Tăng `InpMinBarsBetweenCrosses`
- Tăng `InpSlopeLookbackBars`
- Giữ `SL_HYBRID`
- Muốn **nhiều tín hiệu hơn**:
- Giảm `InpSlopeLookbackBars` xuống 2
- Tắt `InpUseRSISidewayFilter`
- Theo dõi tab **Experts** / **Journal** để xem log `[STATE]`, `[TREND]`, `[PENDING]`, `[TRADE]`.
## 6) Hiển thị trực quan (Visualization)
EA có thể tự vẽ để quan sát và đánh giá (bật/tắt bằng `InpVisualize` và các cờ con).
### 6.1 Dashboard (góc trên-trái)
Hiển thị:
- **Trend**: UP / DOWN / NONE (màu xanh / đỏ / xám)
- **State**: `STATE_NO_TRADE` / `STATE_WATCHING` / `STATE_PENDING_ORDER` / `STATE_IN_TRADE`
- **RSI**: giá trị RSI, EMA9, WMA45 tại nến tín hiệu (`InpSignalBarShift`)
- **EMA200**: giá trị EMA200 + giá đóng hiện tại
- **ATR**: giá trị ATR
- **Context**: tùy trạng thái — ví dụ số nến WATCHING, thời gian sống pending, đã partial hay chưa
### 6.2 Bảng thống kê (góc dưới-trái)
Quét lịch sử `InpStatsLookbackDays` ngày (mặc định 60), lọc theo `InpMagicNumber` + `_Symbol`:
- **Total**: số vị thế đã đóng (gộp theo `POSITION_ID`, tránh đếm trùng partial)
- **TP hit**: số deal đóng với lý do `DEAL_REASON_TP`
- **SL hit**: số deal đóng với lý do `DEAL_REASON_SL`
- **Other**: đóng tay / partial / đóng bởi expert
- **Net PL**: tổng P/L (profit + swap + hoa hồng)
### 6.3 Mức giá Entry / SL / TP (kiểu TradingView)
Khi `STATE_PENDING_ORDER` hoặc `STATE_IN_TRADE`, vẽ 3 đường ngang:
- **ENTRY** (chấm, màu `InpColorEntry`)
- **SL** (gạch, màu `InpColorSL`)
- **TP** (gạch, màu `InpColorTP`)
Tự xóa khi về `NO_TRADE`.
### 6.4 Tự gắn chỉ báo lên chart
Khi `InpAttachIndicators = true` (mặc định):
- **EMA200** vào cửa sổ chính.
- **RSI**, **EMA trên RSI**, **WMA trên RSI** vào **cửa sổ phụ** (subwindow).
Có thể tắt từng phần:
| Tham số | Mặc định | Tác dụng |
| ------- | -------- | -------- |
| `InpVisualize` | true | Bật/tắt toàn bộ lớp hiển thị |
| `InpAttachIndicators` | true | Tự thêm EMA200 + cụm RSI lên chart |
| `InpShowDashboard` | true | Panel góc trên-trái |
| `InpShowStatsPanel` | true | Panel góc dưới-trái |
| `InpShowTradeLevels` | true | Đường Entry/SL/TP |
| `InpStatsLookbackDays` | 60 | Số ngày quét thống kê |
Mọi đối tượng đồ họa dùng tiền tố `RSIForce_` và được xóa trong `OnDeinit`.
## 7) Luồng chạy chi tiết (từng bước)
### 7.1 Toàn cảnh kiến trúc
```
+-------------------------------------------------------------+
| RSIForceStateEA.mq5 |
| (điểm vào — chỉ có OnInit / OnTick / OnTradeTransaction |
| / OnDeinit, không chứa logic nghiệp vụ) |
+-------------------------------------------------------------+
|
| #include theo thứ tự:
v
+-------- Tầng 1 (dữ liệu + kiểu) ---------------------------+
| Config.mqh — toàn bộ input |
| State.mqh — enum EAState, struct SignalSnapshot... |
| Indicators.mqh — handle iRSI/iMA + buffer g_RSI[]... |
+------------------------------------------------------------+
|
| Khai báo global trong .mq5:
| g_State, g_Pending, g_OpenTrade
v
+-------- Tầng 2 (logic) ------------------------------------+
| Trade.mqh — CalcLotsForRisk, PlaceLimitOrderFromPlan,|
| ManagePartialAndBreakEven... |
| StateMachine.mqh — DetectTrend, IsPullback*, IsBuyTrigger*, |
| BuildSignalPlan, RunStateMachine... |
+------------------------------------------------------------+
|
v
+-------- Tầng 3 (giao diện) --------------------------------+
| Visualizer.mqh — DrawDashboardPanel, DrawStatsPanel, |
| DrawTradeLevels, AttachIndicatorsToChart |
+------------------------------------------------------------+
```
### 7.2 Vòng đời — từ lúc gắn EA
#### Bước 0. MT5 gọi `OnInit()` (file `RSIForceStateEA.mq5`)
```
RSIForceStateEA.mq5 :: OnInit()
|
+-- ValidateInputs() (file .mq5 — kiểm tra cấu hình)
| nếu sai -> INIT_PARAMETERS_INCORRECT (EA không khởi động)
|
+-- ZeroMemory(g_Pending), ZeroMemory(g_OpenTrade)
| đưa global về trạng thái sạch
|
+-- InitIndicators() (Indicators.mqh)
| |
| +-- iRSI(...) -> g_hRSI
| +-- iMA(g_hRSI, EMA) -> g_hEMA9 (EMA trên RSI)
| +-- iMA(g_hRSI, WMA) -> g_hWMA45 (WMA trên RSI)
| +-- iMA(close, EMA) -> g_hEMA200 (EMA trên giá đóng)
| +-- iATR(...) -> g_hATR
| +-- ArraySetAsSeries(...) cho mọi buffer
|
+-- RefreshIndicatorData() (Indicators.mqh)
| |
| +-- CopyRates() -> g_Bars[]
| +-- CopyBuffer() -> g_RSI[], g_EMA9[], g_WMA45[], g_EMA200[], g_ATR[]
| nếu chart chưa đủ lịch sử (< 200 nến) -> INIT_FAILED
|
+-- InitTradeOps() (Trade.mqh)
| |
| +-- g_TradeOps.SetExpertMagicNumber(InpMagicNumber)
| +-- g_TradeOps.SetDeviationInPoints(10)
| +-- g_TradeOps.SetTypeFillingBySymbol(_Symbol)
|
+-- g_LastTrend = DetectTrend(InpSignalBarShift)
| khởi tạo trend ngay từ đầu để tick đầu không báo “đảo chiều” giả
|
+-- AttachIndicatorsToChart() (Visualizer.mqh)
| |
| +-- IsIndicatorAlreadyAttached(...) — tránh trùng khi reload EA
| +-- ChartIndicatorAdd(0, 0, g_hEMA200) -> cửa sổ chính
| +-- ChartIndicatorAdd(0, sub, g_hRSI) -> subwindow
| +-- ChartIndicatorAdd(0, sub, g_hEMA9)
| +-- ChartIndicatorAdd(0, sub, g_hWMA45)
|
+-- DrawAllVisuals(g_LastTrend) (Visualizer.mqh)
vẽ dashboard + stats lần đầu
```
#### Bước 1. Mỗi tick — `OnTick()` (`RSIForceStateEA.mq5`)
```
RSIForceStateEA.mq5 :: OnTick()
|
+-- RefreshIndicatorData() (Indicators.mqh)
| cập nhật g_Bars / g_RSI / g_EMA9 / g_WMA45 / g_EMA200 / g_ATR
| nếu copy lỗi (ví dụ thị trường vừa mở) -> return, chờ tick sau
|
+-- SyncStateWithBroker() (StateMachine.mqh)
| |
| nếu state = PENDING_ORDER:
| +- HasOurOpenPosition() = true ? (Trade.mqh)
| | -> chuyển IN_TRADE, copy kế hoạch từ g_Pending sang g_OpenTrade
| +- HasOurPendingOrder() = false (hủy/hết hạn/từ chối) ?
| -> reset g_HasTradedThisPullback, về NO_TRADE
|
| nếu state = IN_TRADE:
| +- HasOurOpenPosition() = false (TP/SL/đóng tay) ?
| -> reset g_HasTradedThisPullback, về NO_TRADE
|
+-- if (state == IN_TRADE):
| ManagePartialAndBreakEven(g_OpenTrade) (Trade.mqh)
| |
| +- PositionSelectByTicket(positionTicket)
| +- profitDist = giá hiện tại so với giá mở (BUY/SELL)
| +- initRisk = |openPrice - SL| (lấy từ broker, không dùng plan)
| +- nếu profitDist >= InpPartialCloseAtR * initRisk:
| - PositionClosePartial(ticket, % volume) nếu chia lot được
| - PositionModify(ticket, openPrice, tp) — SL về BE
| - g_OpenTrade.partialClosedDone = true (chỉ một lần)
|
+-- newBar = IsNewBar() (StateMachine.mqh)
| so sánh g_Bars[0].time với lastBarTime — true khi nến mới mở
|
+-- if (newBar): RunStateMachine() <-- xem Bước 2
|
+-- if (newBar HOẶC đã qua 2 giây kể từ lần vẽ trước):
DrawAllVisuals(DetectTrend(InpSignalBarShift)) (Visualizer.mqh)
|
+- DrawDashboardPanel(trend)
+- DrawStatsPanel()
+- DrawTradeLevels()
```
#### Bước 2. Có nến mới — `RunStateMachine()` (`StateMachine.mqh`)
```
StateMachine.mqh :: RunStateMachine()
|
+-- signalShift = InpSignalBarShift (mặc định = 1, nến vừa đóng)
+-- trendNow = DetectTrend(signalShift) (Up / Down / None)
|
+-- Phát hiện đảo chiều **có hướng** thật (UP <-> DOWN):
| |
| nếu trendNow != NONE:
| +- so với g_LastDirTrend (xu hướng có hướng trước đó)
| +- nếu g_LastDirTrend khác hướng và cả hai đều không NONE
| -> dirFlipped = true
| +- cập nhật g_LastDirTrend = trendNow
|
+-- if (dirFlipped): <-- chỉ reset khi UP <-> DOWN
| +- ResetPullbackCycle()
| +- nếu PENDING_ORDER -> CancelPendingOrder, về NO_TRADE
| +- nếu WATCHING -> về NO_TRADE
|
+-- g_LastTrend = trendNow
|
+-- switch (g_State):
|
case STATE_NO_TRADE:
| HandleStateNoTrade(...)
|
case STATE_WATCHING:
| HandleStateWatching(...)
| (timeout, BuildSignalPlan, PlaceLimitOrderFromPlan...)
|
case STATE_PENDING_ORDER:
| TickPendingOrderLifecycle(...)
| (đếm nến, hết hạn/invalid -> hủy, reset anti-spam)
|
case STATE_IN_TRADE:
(không làm gì ở đây — partial/BE chạy trong OnTick)
```
#### Bước 3. Biến động lệnh — `OnTradeTransaction()`
```
RSIForceStateEA.mq5 :: OnTradeTransaction(...)
|
+-- SyncStateWithBroker()
đồng bộ ngay khi broker báo sự kiện (không chỉ dựa vào tick)
```
#### Bước 4. Gỡ EA — `OnDeinit()`
```
RSIForceStateEA.mq5 :: OnDeinit(reason)
|
+-- RemoveAllVisuals() (Visualizer.mqh)
| ObjectsDeleteAll(0, "RSIForce_")
|
+-- ReleaseIndicators() (Indicators.mqh)
IndicatorRelease cho mọi handle
```
### 7.3 Bảng phân công: file nào làm việc gì
| Trách nhiệm | File | Hàm chính |
| ----------- | ---- | --------- |
| Toàn bộ input | `Config.mqh` | (chỉ khai báo) |
| Enum / struct | `State.mqh` | `EAState`, `TrendDirection`, `SignalSnapshot`, `PendingContext`, `TradeContext` |
| Tạo & cập nhật chỉ báo | `Indicators.mqh` | `InitIndicators`, `RefreshIndicatorData`, `IsBufferSlopingDown/Up`, `HasCrossInLastNBars` |
| Lot, đặt/hủy/quản lý lệnh | `Trade.mqh` | `CalcLotsForRisk`, `PlaceLimitOrderFromPlan`, `CancelPendingOrder`, `ManagePartialAndBreakEven` |
| Trend, pullback, trigger | `StateMachine.mqh` | `DetectTrend`, `IsPullbackInUptrend/Downtrend`, `IsBuyTriggerSignal`, `IsSellTriggerSignal` |
| Điều phối state machine | `StateMachine.mqh` | `RunStateMachine`, `HandleStateNoTrade`, `HandleStateWatching`, `TickPendingOrderLifecycle` |
| Đồng bộ broker | `StateMachine.mqh` | `SyncStateWithBroker` (+ `HasOurOpenPosition` / `HasOurPendingOrder` trong `Trade.mqh`) |
| UI & mức giá | `Visualizer.mqh` | `DrawDashboardPanel`, `DrawStatsPanel`, `DrawTradeLevels`, `AttachIndicatorsToChart` |
| Vòng đời & validate | `RSIForceStateEA.mq5` | `OnInit`, `OnTick`, `OnTradeTransaction`, `OnDeinit`, `ValidateInputs` |
### 7.4 Global của EA và ai được sửa
| Global | Kiểu | Vai trò | Thường sửa tại |
| ------ | ---- | ------- | -------------- |
| `g_State` | `EAState` | Trạng thái SM | `TransitionTo` (StateMachine.mqh) |
| `g_Pending` | `PendingContext` | Lệnh chờ | `PlaceLimitOrderFromPlan`, `CancelPendingOrder` |
| `g_OpenTrade` | `TradeContext` | Vị thế mở | `SyncStateWithBroker`, `ManagePartialAndBreakEven` |
| `g_HasTradedThisPullback` | `bool` | Chống spam | Khi đặt lệnh; reset khi hủy/đóng/đảo chiều |
| `g_BarsInWatching` | `int` | Timeout WATCHING | `HandleStateWatching` và khi chuyển trạng thái |
| `g_LastTrend` | `TrendDirection` | Trend mới nhất | Cuối `RunStateMachine` |
| `g_LastDirTrend` | `TrendDirection` | Trend có hướng gần nhất | `RunStateMachine` khi `trendNow != NONE` |
### 7.5 Sự kiện broker → cách code phản ứng
| Sự kiện thực tế | Cách phát hiện | Phản ứng trong code |
| --------------- | -------------- | ------------------- |
| Limit khớp | `OnTradeTransaction` + `SyncStateWithBroker` | `PENDING``IN_TRADE`, copy plan |
| Pending hết hạn / bị hủy | `Sync` (không còn order) | `PENDING``NO_TRADE`, reset anti-spam |
| Pending bị từ chối | `PlaceLimit...` false hoặc `Sync` | Ở `NO_TRADE`, có log retcode |
| Chạm TP | `Sync` (không còn position) | `IN_TRADE``NO_TRADE`, reset anti-spam |
| Chạm SL | như trên | như trên |
| Đóng tay | như trên | như trên |
| Đạt mức partial R | `OnTick``ManagePartialAndBreakEven` | Partial (nếu được) + SL về BE |
| Đảo chiều UP ↔ DOWN | `RunStateMachine``dirFlipped` | Hủy pending ngược hướng, reset chu kỳ |
| Đi qua NONE rồi về cùng hướng | `RunStateMachine` | **Không** coi là flip; `g_LastDirTrend` giữ ngữ cảnh |
| Nến mới | `OnTick``IsNewBar` | Gọi `RunStateMachine` một lần |
## 8) Ghi chú kiến trúc
- State machine chạy theo **nến mới** (`IsNewBar()`), tránh bắn tín hiệu nhiều lần trong cùng một nến.
- Quản lý vị thế (partial + BE) chạy **mỗi tick** để phản ứng nhanh theo giá.
- `SyncStateWithBroker()` chạy mỗi tick và sau `OnTradeTransaction` để trạng thái nội bộ luôn khớp broker (đóng tay, từ chối lệnh, v.v.).
- Phân tầng `#include`:
- **Tầng 1** chỉ khai báo dữ liệu và hàm thuần.
- **Tầng 2** dùng các global khai báo giữa hai tầng.
- **Tầng 3** chỉ đọc/hiển thị, không sửa logic nghiệp vụ cốt lõi.
---
*Tệp README dùng mã hóa UTF-8. Tiếng Việt có dấu an toàn với Git, GitHub và trình soạn thảo hiện đại; không ảnh hưởng tới biên dịch EA (MetaEditor không compile file `.md`).*
-52
View File
@@ -1,52 +0,0 @@
#ifndef RSI_FORCE_STATE_EA__STATE_MQH
#define RSI_FORCE_STATE_EA__STATE_MQH
// ============================================================
// State machine enums + lightweight value types (no globals)
// ============================================================
enum EAState
{
STATE_NO_TRADE = 0, // khong co setup
STATE_WATCHING = 1, // co pullback hop le, cho trigger
STATE_PENDING_ORDER = 2, // da dat limit, cho khop
STATE_IN_TRADE = 3 // da khop, dang quan ly position
};
enum TrendDirection
{
TREND_NONE = 0,
TREND_UP = +1,
TREND_DOWN = -1
};
// Snapshot of an entry plan computed at the signal bar.
// All prices are already normalized to broker digits.
struct SignalSnapshot
{
datetime signalBarTime; // open time of the signal bar
int direction; // +1 buy, -1 sell
double entryPrice;
double stopLossPrice;
double takeProfitPrice;
double initialRiskPrice; // |entry - SL| in price units
};
// Track a live pending limit order.
struct PendingContext
{
ulong orderTicket;
int barsSincePlaced;
SignalSnapshot plan;
};
// Track an open position created from a filled pending order.
struct TradeContext
{
bool isActive;
ulong positionTicket;
bool partialClosedDone; // true after 1.5R partial + BE move
SignalSnapshot plan;
};
#endif
-371
View File
@@ -1,371 +0,0 @@
#ifndef RSI_FORCE_STATE_EA__STATE_MACHINE_MQH
#define RSI_FORCE_STATE_EA__STATE_MACHINE_MQH
// ============================================================
// State machine: NO_TRADE -> WATCHING -> PENDING_ORDER -> IN_TRADE
// ============================================================
// Anti-spam guard: at most one trade per pullback. Reset when:
// - direction-trend flips (UP <-> DOWN)
// - pending order is cancelled (no actual trade happened)
// - the open trade fully closes (TP/SL/manual)
bool g_HasTradedThisPullback = false;
int g_BarsInWatching = 0;
// Two trend trackers:
// g_LastTrend - exact value last seen (UP / DOWN / NONE), for context
// g_LastDirTrend - last DIRECTIONAL value (UP / DOWN). Used to detect a real
// directional flip even if the trend briefly went through NONE.
TrendDirection g_LastTrend = TREND_NONE;
TrendDirection g_LastDirTrend = TREND_NONE;
// ------------------------------------------------------------
// Misc helpers
// ------------------------------------------------------------
void TransitionTo(const EAState nextState)
{
if (g_State == nextState) return;
if (InpDebugLog)
PrintFormat("[STATE] %s -> %s", EnumToString(g_State), EnumToString(nextState));
g_State = nextState;
}
bool IsNewBar()
{
static datetime lastBarTime = 0;
if (g_Bars[0].time == 0) return false;
if (g_Bars[0].time == lastBarTime) return false;
lastBarTime = g_Bars[0].time;
return true;
}
// ------------------------------------------------------------
// Trend filter (EMA200 with optional dead-zone + flat guard)
// ------------------------------------------------------------
TrendDirection DetectTrend(const int signalShift)
{
const double closePrice = g_Bars[signalShift].close;
const double ema200 = g_EMA200[signalShift];
const double atr = g_ATR[signalShift];
if (ema200 <= 0.0) return TREND_NONE;
// Dead-zone around EMA200 to avoid noise.
double bufferPrice = ema200 * (InpTrendBufferPercent / 100.0);
if (InpUseATRTrendBuffer) bufferPrice = atr * InpTrendBufferATRMult;
if (bufferPrice <= 0.0) bufferPrice = 5.0 * _Point;
// Flat EMA200 guard: skip when EMA200 barely moves between two bars.
if (InpSkipFlatEMA200)
{
const double emaDelta = MathAbs(g_EMA200[signalShift] - g_EMA200[signalShift + 1]);
const double flatThreshold = MathMax(_Point, atr * InpFlatEMA_ATRMult);
if (emaDelta <= flatThreshold) return TREND_NONE;
}
if (closePrice > ema200 + bufferPrice) return TREND_UP;
if (closePrice < ema200 - bufferPrice) return TREND_DOWN;
return TREND_NONE;
}
// All N RSI values inside [low, high] band -> sideway.
bool IsRSISideway(const int signalShift)
{
if (!InpUseRSISidewayFilter) return false;
for (int i = signalShift; i < signalShift + InpSidewayLookbackBars; i++)
{
if (g_RSI[i] < InpSidewayRSILow || g_RSI[i] > InpSidewayRSIHigh)
return false;
}
return true;
}
// ------------------------------------------------------------
// Pullback (state NO_TRADE -> WATCHING) and trigger (WATCHING -> PENDING)
// ------------------------------------------------------------
bool IsPullbackInUptrend(const int signalShift)
{
// RSI < EMA9 < WMA45 AND all three buffers slope down (current pullback).
return (g_RSI[signalShift] < g_EMA9[signalShift]
&& g_EMA9[signalShift] < g_WMA45[signalShift]
&& IsBufferSlopingDown(g_RSI, signalShift, InpSlopeLookbackBars)
&& IsBufferSlopingDown(g_EMA9, signalShift, InpSlopeLookbackBars)
&& IsBufferSlopingDown(g_WMA45, signalShift, InpSlopeLookbackBars));
}
bool IsPullbackInDowntrend(const int signalShift)
{
// RSI > EMA9 > WMA45 AND all three buffers slope up.
return (g_RSI[signalShift] > g_EMA9[signalShift]
&& g_EMA9[signalShift] > g_WMA45[signalShift]
&& IsBufferSlopingUp(g_RSI, signalShift, InpSlopeLookbackBars)
&& IsBufferSlopingUp(g_EMA9, signalShift, InpSlopeLookbackBars)
&& IsBufferSlopingUp(g_WMA45, signalShift, InpSlopeLookbackBars));
}
// BUY trigger: RSI just crossed up WMA45 + EMA9 still below WMA45
// + RSI didn't cross WMA45 in the previous N bars.
bool IsBuyTriggerSignal(const int signalShift)
{
const bool rsiCrossedUp = (g_RSI[signalShift + 1] <= g_WMA45[signalShift + 1]
&& g_RSI[signalShift] > g_WMA45[signalShift]);
const bool ema9StillBelow = (g_EMA9[signalShift] < g_WMA45[signalShift]);
const bool wasIsolated = !HasCrossInLastNBars(g_RSI, g_WMA45,
signalShift + 2,
InpMinBarsBetweenCrosses);
return rsiCrossedUp && ema9StillBelow && wasIsolated;
}
// SELL trigger: mirror of BUY.
bool IsSellTriggerSignal(const int signalShift)
{
const bool rsiCrossedDown = (g_RSI[signalShift + 1] >= g_WMA45[signalShift + 1]
&& g_RSI[signalShift] < g_WMA45[signalShift]);
const bool ema9StillAbove = (g_EMA9[signalShift] > g_WMA45[signalShift]);
const bool wasIsolated = !HasCrossInLastNBars(g_RSI, g_WMA45,
signalShift + 2,
InpMinBarsBetweenCrosses);
return rsiCrossedDown && ema9StillAbove && wasIsolated;
}
// ------------------------------------------------------------
// Build a complete entry plan (entry / SL / TP / direction / risk)
// ------------------------------------------------------------
bool BuildSignalPlan(const int direction, const int signalShift, SignalSnapshot &outPlan)
{
// Entry = midpoint between signal close and the nearest swing extreme:
// BUY -> midpoint between close and nearest swing low.
// SELL -> midpoint between close and nearest swing high.
const double swingAnchor = FindNearestSwingForEntry(direction, signalShift);
if (swingAnchor <= 0.0) return false;
const double closePrice = g_Bars[signalShift].close;
const double entryPrice = (closePrice + swingAnchor) * 0.5;
// Sanity: BUY limit must sit below close, SELL limit above close.
if (direction > 0 && entryPrice >= closePrice) return false;
if (direction < 0 && entryPrice <= closePrice) return false;
const double slPrice = ComputeStopLossPrice(direction, signalShift,
entryPrice, g_ATR[signalShift]);
if (direction > 0 && slPrice >= entryPrice) return false;
if (direction < 0 && slPrice <= entryPrice) return false;
const double riskInPrice = MathAbs(entryPrice - slPrice);
if (riskInPrice <= (2.0 * _Point)) return false;
outPlan.signalBarTime = g_Bars[signalShift].time;
outPlan.direction = direction;
outPlan.entryPrice = NormalizePriceToTick(entryPrice);
outPlan.stopLossPrice = NormalizePriceToTick(slPrice);
outPlan.initialRiskPrice = riskInPrice;
outPlan.takeProfitPrice = NormalizePriceToTick((direction > 0)
? (entryPrice + InpRiskRewardRatio * riskInPrice)
: (entryPrice - InpRiskRewardRatio * riskInPrice));
return true;
}
// ------------------------------------------------------------
// Broker-state synchronization
// ------------------------------------------------------------
void ResetPullbackCycle()
{
g_HasTradedThisPullback = false;
g_BarsInWatching = 0;
}
// Reconcile internal state with the broker:
// PENDING_ORDER -> IN_TRADE if a position appears
// PENDING_ORDER -> NO_TRADE if order vanished without fill (cancel/reject/expire)
// IN_TRADE -> NO_TRADE if position no longer exists (TP/SL/manual)
// On both NO_TRADE transitions, also clear g_HasTradedThisPullback so the EA
// can take the next pullback opportunity in the same trend.
void SyncStateWithBroker()
{
ulong posTicket = 0;
const bool hasPos = HasOurOpenPosition(posTicket);
const bool hasPending = HasOurPendingOrder(g_Pending.orderTicket);
if (g_State == STATE_PENDING_ORDER)
{
if (hasPos)
{
g_OpenTrade.isActive = true;
g_OpenTrade.positionTicket = posTicket;
g_OpenTrade.plan = g_Pending.plan;
g_OpenTrade.partialClosedDone = false;
g_Pending.orderTicket = 0;
g_Pending.barsSincePlaced = 0;
TransitionTo(STATE_IN_TRADE);
return;
}
if (!hasPending)
{
// Pending was cancelled / expired / rejected externally.
g_Pending.orderTicket = 0;
g_Pending.barsSincePlaced = 0;
g_HasTradedThisPullback = false; // allow the next setup attempt
TransitionTo(STATE_NO_TRADE);
}
}
else if (g_State == STATE_IN_TRADE)
{
if (!hasPos)
{
g_OpenTrade.isActive = false;
g_OpenTrade.positionTicket = 0;
g_HasTradedThisPullback = false; // trade done -> allow next setup
TransitionTo(STATE_NO_TRADE);
}
}
}
// ------------------------------------------------------------
// Pending order lifecycle on every newly closed bar
// ------------------------------------------------------------
void TickPendingOrderLifecycle(const TrendDirection trendNow)
{
if (g_State != STATE_PENDING_ORDER || g_Pending.orderTicket == 0) return;
g_Pending.barsSincePlaced++;
const bool expired = (g_Pending.barsSincePlaced >= InpPendingMaxAliveBars);
bool invalidated = false;
if (InpInvalidateIfCrossBack)
{
if (g_Pending.plan.direction > 0)
invalidated = (trendNow != TREND_UP || g_RSI[1] < g_WMA45[1]);
else
invalidated = (trendNow != TREND_DOWN || g_RSI[1] > g_WMA45[1]);
}
if (!expired && !invalidated) return;
if (CancelPendingOrder(g_Pending))
{
if (InpDebugLog)
PrintFormat("[PENDING] cancelled (%s)", expired ? "expired" : "invalidated");
g_HasTradedThisPullback = false; // allow next pullback to retry
TransitionTo(STATE_NO_TRADE);
}
}
// ------------------------------------------------------------
// Per-state handlers
// ------------------------------------------------------------
void HandleStateNoTrade(const int signalShift, const TrendDirection trendNow)
{
if (g_HasTradedThisPullback) return; // anti-spam: wait until reset
if (IsRSISideway(signalShift)) return; // sideway filter only blocks new setups
if (trendNow == TREND_NONE) return; // need a real direction
if (trendNow == TREND_UP && IsPullbackInUptrend(signalShift))
{
g_BarsInWatching = 0;
TransitionTo(STATE_WATCHING);
}
else if (trendNow == TREND_DOWN && IsPullbackInDowntrend(signalShift))
{
g_BarsInWatching = 0;
TransitionTo(STATE_WATCHING);
}
}
void HandleStateWatching(const int signalShift, const TrendDirection trendNow)
{
g_BarsInWatching++;
// Trend lost while watching -> abandon setup.
if (trendNow == TREND_NONE)
{
TransitionTo(STATE_NO_TRADE);
return;
}
// Watching too long without a trigger -> abandon to avoid stale setups.
if (g_BarsInWatching > InpWatchingMaxBars)
{
if (InpDebugLog) Print("[WATCHING] timed out, back to NO_TRADE");
TransitionTo(STATE_NO_TRADE);
return;
}
// Try to trigger an entry on this bar.
SignalSnapshot plan;
ZeroMemory(plan);
bool hasSignal = false;
if (trendNow == TREND_UP && IsBuyTriggerSignal(signalShift))
hasSignal = BuildSignalPlan(+1, signalShift, plan);
else if (trendNow == TREND_DOWN && IsSellTriggerSignal(signalShift))
hasSignal = BuildSignalPlan(-1, signalShift, plan);
if (!hasSignal) return;
if (PlaceLimitOrderFromPlan(plan, g_Pending))
{
g_HasTradedThisPullback = true;
TransitionTo(STATE_PENDING_ORDER);
}
}
// ------------------------------------------------------------
// Top-level entry point: called once per closed bar
// ------------------------------------------------------------
void RunStateMachine()
{
const int signalShift = InpSignalBarShift;
const TrendDirection trendNow = DetectTrend(signalShift);
// Detect a TRUE directional flip (UP <-> DOWN).
// A short trip through TREND_NONE between two same-direction trends
// is NOT a flip and must NOT reset the cycle / abandon WATCHING.
bool dirFlipped = false;
if (trendNow != TREND_NONE)
{
if (g_LastDirTrend != TREND_NONE && g_LastDirTrend != trendNow)
dirFlipped = true;
g_LastDirTrend = trendNow;
}
if (dirFlipped)
{
if (InpDebugLog)
PrintFormat("[TREND] direction flipped %s -> %s",
EnumToString(g_LastTrend), EnumToString(trendNow));
ResetPullbackCycle();
if (g_State == STATE_PENDING_ORDER && g_Pending.orderTicket > 0)
{
if (CancelPendingOrder(g_Pending))
{
if (InpDebugLog) Print("[PENDING] cancelled by trend flip");
TransitionTo(STATE_NO_TRADE);
}
}
else if (g_State == STATE_WATCHING)
{
TransitionTo(STATE_NO_TRADE);
}
}
g_LastTrend = trendNow;
switch (g_State)
{
case STATE_NO_TRADE: HandleStateNoTrade(signalShift, trendNow); break;
case STATE_WATCHING: HandleStateWatching(signalShift, trendNow); break;
case STATE_PENDING_ORDER: TickPendingOrderLifecycle(trendNow); break;
case STATE_IN_TRADE: /* tick-level handler does the work */ break;
}
}
#endif
-328
View File
@@ -1,328 +0,0 @@
#ifndef RSI_FORCE_STATE_EA__TRADE_MQH
#define RSI_FORCE_STATE_EA__TRADE_MQH
#include <Trade/Trade.mqh>
// CTrade wrapper used for all order/position operations.
// Defaults (magic, deviation) are set once in InitTradeOps() at OnInit.
CTrade g_TradeOps;
// ------------------------------------------------------------
// Lifecycle
// ------------------------------------------------------------
void InitTradeOps()
{
g_TradeOps.SetExpertMagicNumber(InpMagicNumber);
g_TradeOps.SetDeviationInPoints(10);
g_TradeOps.SetTypeFillingBySymbol(_Symbol);
}
// ------------------------------------------------------------
// Symbol primitives
// ------------------------------------------------------------
double NormalizePriceToTick(const double price)
{
return NormalizeDouble(price, (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS));
}
double NormalizeVolumeToBroker(const double rawVolume)
{
const double volMin = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
const double volMax = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
const double volStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
if (volStep <= 0.0) return 0.0;
double v = MathFloor(rawVolume / volStep) * volStep;
v = MathMax(v, volMin);
v = MathMin(v, volMax);
return NormalizeDouble(v, 2);
}
double GetStopsLevelPrice()
{
// Broker minimum distance for SL/TP/limit price from market.
const long stopsLevelPoints = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
return (double)stopsLevelPoints * _Point;
}
// Position sizing from fixed % of balance and price distance entry->SL.
double CalcLotsForRisk(const double entryPrice, const double stopLossPrice)
{
const double balance = AccountInfoDouble(ACCOUNT_BALANCE);
const double riskMoney = balance * (InpRiskPercent / 100.0);
const double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
const double tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
const double slDistance = MathAbs(entryPrice - stopLossPrice);
if (slDistance <= 0.0 || tickValue <= 0.0 || tickSize <= 0.0) return 0.0;
const double moneyPerLot = (slDistance / tickSize) * tickValue;
if (moneyPerLot <= 0.0) return 0.0;
return NormalizeVolumeToBroker(riskMoney / moneyPerLot);
}
// ------------------------------------------------------------
// Swing helpers (used both for entry anchor and SL anchor)
// ------------------------------------------------------------
// Returns the raw extreme price of the nearest swing (no buffer added).
// Used to compute the entry midpoint = (close + swingExtreme) / 2.
double FindNearestSwingForEntry(const int direction, const int signalShift)
{
const int startShift = signalShift + 1;
const int lookback = MathMax(5, InpSwingLookbackBars);
if (direction > 0)
{
const int swingIdx = iLowest(_Symbol, _Period, MODE_LOW, lookback, startShift);
if (swingIdx <= 0) return 0.0;
return g_Bars[swingIdx].low;
}
const int swingIdx = iHighest(_Symbol, _Period, MODE_HIGH, lookback, startShift);
if (swingIdx <= 0) return 0.0;
return g_Bars[swingIdx].high;
}
// Returns the SL price anchored to the nearest swing extreme + safety buffer.
double FindNearestSwingForSL(const int direction, const int signalShift)
{
const int startShift = signalShift + 1;
const int lookback = MathMax(5, InpSwingLookbackBars);
if (direction > 0)
{
const int swingIdx = iLowest(_Symbol, _Period, MODE_LOW, lookback, startShift);
if (swingIdx <= 0) return 0.0;
return g_Bars[swingIdx].low - (InpSL_SwingBufferPoints * _Point);
}
const int swingIdx = iHighest(_Symbol, _Period, MODE_HIGH, lookback, startShift);
if (swingIdx <= 0) return 0.0;
return g_Bars[swingIdx].high + (InpSL_SwingBufferPoints * _Point);
}
// Compose final SL price honoring InpStopLossMode.
double ComputeStopLossPrice(const int direction, const int signalShift,
const double entryPrice, const double atrValue)
{
const double swingSL = FindNearestSwingForSL(direction, signalShift);
const double atrSL = (direction > 0)
? entryPrice - (atrValue * InpSL_ATRMult)
: entryPrice + (atrValue * InpSL_ATRMult);
if (InpStopLossMode == SL_SWING && swingSL > 0.0) return swingSL;
if (InpStopLossMode == SL_ATR) return atrSL;
if (swingSL <= 0.0) return atrSL;
// SL_HYBRID: pick the wider (safer) stop on the correct side.
return (direction > 0) ? MathMin(swingSL, atrSL) : MathMax(swingSL, atrSL);
}
// ------------------------------------------------------------
// Order placement / cancellation
// ------------------------------------------------------------
// Validates that the limit price + SL/TP respect the broker's stops level.
// For BUY LIMIT, entry must be below current Ask by at least stopsLevel.
// For SELL LIMIT, entry must be above current Bid by at least stopsLevel.
bool ValidateLimitPrices(const SignalSnapshot &plan)
{
const double stopsLevel = GetStopsLevelPrice();
const double askNow = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
const double bidNow = SymbolInfoDouble(_Symbol, SYMBOL_BID);
if (plan.direction > 0)
{
if (plan.entryPrice > askNow - stopsLevel)
{
if (InpDebugLog)
PrintFormat("[ORDER] reject BUY LIMIT entry=%.5f too close to ask=%.5f (stops=%.5f)",
plan.entryPrice, askNow, stopsLevel);
return false;
}
if (plan.entryPrice - plan.stopLossPrice < stopsLevel
|| plan.takeProfitPrice - plan.entryPrice < stopsLevel)
{
if (InpDebugLog)
PrintFormat("[ORDER] reject BUY LIMIT SL/TP too close to entry (stops=%.5f)", stopsLevel);
return false;
}
}
else
{
if (plan.entryPrice < bidNow + stopsLevel)
{
if (InpDebugLog)
PrintFormat("[ORDER] reject SELL LIMIT entry=%.5f too close to bid=%.5f (stops=%.5f)",
plan.entryPrice, bidNow, stopsLevel);
return false;
}
if (plan.stopLossPrice - plan.entryPrice < stopsLevel
|| plan.entryPrice - plan.takeProfitPrice < stopsLevel)
{
if (InpDebugLog)
PrintFormat("[ORDER] reject SELL LIMIT SL/TP too close to entry (stops=%.5f)", stopsLevel);
return false;
}
}
return true;
}
bool PlaceLimitOrderFromPlan(const SignalSnapshot &plan, PendingContext &pendingCtx)
{
if (!ValidateLimitPrices(plan)) return false;
const double lots = CalcLotsForRisk(plan.entryPrice, plan.stopLossPrice);
if (lots <= 0.0)
{
if (InpDebugLog)
PrintFormat("[ORDER] reject: lots=%.4f (risk too small or symbol info missing)", lots);
return false;
}
const string comment = (plan.direction > 0) ? "RSIForce_BUY_LIMIT" : "RSIForce_SELL_LIMIT";
bool placed = false;
if (plan.direction > 0)
placed = g_TradeOps.BuyLimit(lots,
NormalizePriceToTick(plan.entryPrice), _Symbol,
NormalizePriceToTick(plan.stopLossPrice),
NormalizePriceToTick(plan.takeProfitPrice),
ORDER_TIME_GTC, 0, comment);
else
placed = g_TradeOps.SellLimit(lots,
NormalizePriceToTick(plan.entryPrice), _Symbol,
NormalizePriceToTick(plan.stopLossPrice),
NormalizePriceToTick(plan.takeProfitPrice),
ORDER_TIME_GTC, 0, comment);
if (!placed)
{
if (InpDebugLog)
PrintFormat("[ORDER] place fail: ret=%u msg=%s",
g_TradeOps.ResultRetcode(), g_TradeOps.ResultRetcodeDescription());
return false;
}
pendingCtx.orderTicket = g_TradeOps.ResultOrder();
pendingCtx.barsSincePlaced = 0;
pendingCtx.plan = plan;
if (InpDebugLog)
PrintFormat("[ORDER] placed %s lots=%.2f entry=%.5f SL=%.5f TP=%.5f ticket=%I64u",
comment, lots, plan.entryPrice, plan.stopLossPrice, plan.takeProfitPrice,
pendingCtx.orderTicket);
return (pendingCtx.orderTicket > 0);
}
bool CancelPendingOrder(PendingContext &pendingCtx)
{
if (pendingCtx.orderTicket == 0) return true;
if (!g_TradeOps.OrderDelete(pendingCtx.orderTicket))
{
if (InpDebugLog)
PrintFormat("[ORDER] delete fail: ret=%u", g_TradeOps.ResultRetcode());
return false;
}
pendingCtx.orderTicket = 0;
pendingCtx.barsSincePlaced = 0;
return true;
}
// ------------------------------------------------------------
// Broker queries (filtered by symbol + magic)
// ------------------------------------------------------------
bool HasOurOpenPosition(ulong &outTicket)
{
for (int i = PositionsTotal() - 1; i >= 0; i--)
{
const ulong posTicket = PositionGetTicket(i);
if (posTicket <= 0) continue;
if (PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
if (PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) continue;
outTicket = posTicket;
return true;
}
return false;
}
bool HasOurPendingOrder(const ulong ticket)
{
if (ticket == 0) return false;
for (int i = OrdersTotal() - 1; i >= 0; i--)
{
if (OrderGetTicket(i) == ticket) return true;
}
return false;
}
// ------------------------------------------------------------
// Trade management: partial close at +R + move SL to BE
// ------------------------------------------------------------
bool ManagePartialAndBreakEven(TradeContext &openTrade)
{
if (!openTrade.isActive || openTrade.partialClosedDone) return true;
if (!PositionSelectByTicket(openTrade.positionTicket)) return false;
if (PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) return false;
const long posType = PositionGetInteger(POSITION_TYPE);
const double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);
const double slPrice = PositionGetDouble(POSITION_SL);
const double tpPrice = PositionGetDouble(POSITION_TP);
const double posVolume = PositionGetDouble(POSITION_VOLUME);
// Use the ACTUAL fill->SL distance as R, not the planned one
// (broker fill price may differ from planned entry price).
const double initRisk = MathAbs(openPrice - slPrice);
if (initRisk <= 0.0) return false;
const double priceNow = (posType == POSITION_TYPE_BUY)
? SymbolInfoDouble(_Symbol, SYMBOL_BID)
: SymbolInfoDouble(_Symbol, SYMBOL_ASK);
const double profitDist = (posType == POSITION_TYPE_BUY)
? (priceNow - openPrice)
: (openPrice - priceNow);
// Not yet at the partial trigger.
if (profitDist < (InpPartialCloseAtR * initRisk)) return true;
// Try to split off `InpPartialClosePercent`% but only if both sides
// remain >= volMin after split (otherwise just move BE).
const double volMin = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
const double rawClose = posVolume * (InpPartialClosePercent / 100.0);
const double closeVol = NormalizeVolumeToBroker(rawClose);
const double remainVol = NormalizeVolumeToBroker(posVolume - closeVol);
const bool canSplit = (closeVol >= volMin)
&& (remainVol >= volMin)
&& (closeVol < posVolume);
if (canSplit)
{
if (!g_TradeOps.PositionClosePartial(openTrade.positionTicket, closeVol))
{
if (InpDebugLog)
PrintFormat("[TRADE] partial-close fail: ret=%u", g_TradeOps.ResultRetcode());
return false;
}
}
// Always move SL to the actual fill price (BE).
if (!g_TradeOps.PositionModify(openTrade.positionTicket,
NormalizePriceToTick(openPrice), tpPrice))
{
if (InpDebugLog)
PrintFormat("[TRADE] BE-move fail: ret=%u", g_TradeOps.ResultRetcode());
return false;
}
openTrade.partialClosedDone = true;
if (InpDebugLog)
PrintFormat("[TRADE] Partial=%s vol=%.2f -> SL moved to BE @ %.5f",
canSplit ? "yes" : "skipped(min vol)", closeVol, openPrice);
return true;
}
#endif
-366
View File
@@ -1,366 +0,0 @@
#ifndef RSI_FORCE_STATE_EA__VISUALIZER_MQH
#define RSI_FORCE_STATE_EA__VISUALIZER_MQH
// ============================================================
// Visual layer for RSIForceStateEA.
// Renders:
// - Top-left dashboard (trend, state, key indicator values).
// - Bottom-left stats panel (totals, TP, SL, net P/L).
// - TradingView-style Entry/SL/TP horizontal lines with labels.
// - Optionally attaches EMA200/RSI/EMA9/WMA45 to the chart.
// All visual objects share the prefix "RSIForce_" and are removed
// in OnDeinit via RemoveAllVisuals().
// ============================================================
#define VIS_PREFIX "RSIForce_"
#define VIS_LBL_DASH_PREFIX VIS_PREFIX "DASH_"
#define VIS_LBL_STATS_PREFIX VIS_PREFIX "STATS_"
#define VIS_OBJ_LEVEL_PREFIX VIS_PREFIX "LV_"
// Aggregated stats computed from the deal history.
struct VisualTradeStats
{
int totalClosed; // unique closed positions (any reason)
int tpHits; // close deals with reason TP
int slHits; // close deals with reason SL
int otherCloses; // manual / expert / partial
double netPL; // sum of profit on close deals
};
// ------------------------------------------------------------
// Generic object helpers
// ------------------------------------------------------------
void EnsureLabel(const string objName, const int xDist, const int yDist,
const ENUM_BASE_CORNER corner, const color clr,
const string text, const int fontSize = 9)
{
if (ObjectFind(0, objName) < 0)
{
ObjectCreate(0, objName, OBJ_LABEL, 0, 0, 0);
ObjectSetInteger(0, objName, OBJPROP_CORNER, corner);
ObjectSetInteger(0, objName, OBJPROP_XDISTANCE, xDist);
ObjectSetInteger(0, objName, OBJPROP_YDISTANCE, yDist);
ObjectSetInteger(0, objName, OBJPROP_FONTSIZE, fontSize);
ObjectSetString (0, objName, OBJPROP_FONT, "Consolas");
ObjectSetInteger(0, objName, OBJPROP_BACK, false);
ObjectSetInteger(0, objName, OBJPROP_SELECTABLE, false);
ObjectSetInteger(0, objName, OBJPROP_HIDDEN, true);
}
ObjectSetInteger(0, objName, OBJPROP_COLOR, clr);
ObjectSetString (0, objName, OBJPROP_TEXT, text);
}
void EnsureHLine(const string objName, const double price, const color clr,
const ENUM_LINE_STYLE style, const int width, const string text)
{
if (ObjectFind(0, objName) < 0)
{
ObjectCreate(0, objName, OBJ_HLINE, 0, 0, price);
ObjectSetInteger(0, objName, OBJPROP_BACK, true);
ObjectSetInteger(0, objName, OBJPROP_SELECTABLE, false);
ObjectSetInteger(0, objName, OBJPROP_HIDDEN, true);
}
ObjectSetDouble (0, objName, OBJPROP_PRICE, price);
ObjectSetInteger(0, objName, OBJPROP_COLOR, clr);
ObjectSetInteger(0, objName, OBJPROP_STYLE, style);
ObjectSetInteger(0, objName, OBJPROP_WIDTH, width);
ObjectSetString (0, objName, OBJPROP_TEXT, text);
}
color TrendColor(const TrendDirection trend)
{
if (trend == TREND_UP) return InpColorTrendUp;
if (trend == TREND_DOWN) return InpColorTrendDown;
return InpColorTrendNone;
}
string TrendLabel(const TrendDirection trend)
{
if (trend == TREND_UP) return "UP";
if (trend == TREND_DOWN) return "DOWN";
return "NONE";
}
// ------------------------------------------------------------
// Top-left dashboard
// ------------------------------------------------------------
void DrawDashboardPanel(const TrendDirection trendNow)
{
if (!InpShowDashboard) return;
const int xLeft = 12;
int y = 12;
const int rowH = 18;
EnsureLabel(VIS_LBL_DASH_PREFIX "title", xLeft, y, CORNER_LEFT_UPPER,
clrWhite, "RSIForceStateEA", 11); y += rowH;
EnsureLabel(VIS_LBL_DASH_PREFIX "trend", xLeft, y, CORNER_LEFT_UPPER,
TrendColor(trendNow),
"Trend : " + TrendLabel(trendNow)); y += rowH;
color stateClr = (g_State == STATE_IN_TRADE) ? clrLime
: (g_State == STATE_PENDING_ORDER) ? clrOrange
: (g_State == STATE_WATCHING) ? clrYellow
: clrSilver;
EnsureLabel(VIS_LBL_DASH_PREFIX "state", xLeft, y, CORNER_LEFT_UPPER,
stateClr,
"State : " + EnumToString(g_State)); y += rowH;
EnsureLabel(VIS_LBL_DASH_PREFIX "rsi", xLeft, y, CORNER_LEFT_UPPER,
clrAqua,
StringFormat("RSI : %6.2f EMA9 : %6.2f WMA45: %6.2f",
g_RSI[1], g_EMA9[1], g_WMA45[1])); y += rowH;
EnsureLabel(VIS_LBL_DASH_PREFIX "ema200", xLeft, y, CORNER_LEFT_UPPER,
InpColorEntry,
StringFormat("EMA200 : %.5f Close: %.5f",
g_EMA200[1], g_Bars[1].close)); y += rowH;
EnsureLabel(VIS_LBL_DASH_PREFIX "atr", xLeft, y, CORNER_LEFT_UPPER,
clrSilver,
StringFormat("ATR : %.5f", g_ATR[1])); y += rowH;
string contextLine = "";
if (g_State == STATE_PENDING_ORDER)
contextLine = StringFormat("Pending: dir=%s entry=%.5f alive=%d/%d",
(g_Pending.plan.direction > 0 ? "BUY" : "SELL"),
g_Pending.plan.entryPrice,
g_Pending.barsSincePlaced,
InpPendingMaxAliveBars);
else if (g_State == STATE_IN_TRADE)
contextLine = StringFormat("Trade : dir=%s entry=%.5f partial=%s",
(g_OpenTrade.plan.direction > 0 ? "BUY" : "SELL"),
g_OpenTrade.plan.entryPrice,
g_OpenTrade.partialClosedDone ? "DONE" : "PEND");
else if (g_State == STATE_WATCHING)
contextLine = StringFormat("Watch : %d/%d bars",
g_BarsInWatching, InpWatchingMaxBars);
else
contextLine = "Idle : looking for pullback...";
EnsureLabel(VIS_LBL_DASH_PREFIX "context", xLeft, y, CORNER_LEFT_UPPER,
clrOrange, contextLine);
}
// ------------------------------------------------------------
// Bottom-left stats panel
// ------------------------------------------------------------
bool IsDealOurs(const ulong dealTicket)
{
if (HistoryDealGetInteger(dealTicket, DEAL_MAGIC) != InpMagicNumber) return false;
if (HistoryDealGetString (dealTicket, DEAL_SYMBOL) != _Symbol) return false;
return true;
}
bool ContainsULong(const ulong &arr[], const int count, const ulong needle)
{
for (int i = 0; i < count; i++)
if (arr[i] == needle) return true;
return false;
}
void ComputeTradeStats(VisualTradeStats &stats)
{
ZeroMemory(stats);
const datetime fromTime = TimeCurrent() - (datetime)(InpStatsLookbackDays * 86400);
if (!HistorySelect(fromTime, TimeCurrent())) return;
ulong seenPositions[];
int seenCount = 0;
const int dealsTotal = HistoryDealsTotal();
for (int i = 0; i < dealsTotal; i++)
{
const ulong dealTicket = HistoryDealGetTicket(i);
if (dealTicket == 0) continue;
if (!IsDealOurs(dealTicket)) continue;
if (HistoryDealGetInteger(dealTicket, DEAL_ENTRY) != DEAL_ENTRY_OUT) continue;
const long reason = HistoryDealGetInteger(dealTicket, DEAL_REASON);
const ulong positionId = (ulong)HistoryDealGetInteger(dealTicket, DEAL_POSITION_ID);
const double profit = HistoryDealGetDouble (dealTicket, DEAL_PROFIT)
+ HistoryDealGetDouble (dealTicket, DEAL_SWAP)
+ HistoryDealGetDouble (dealTicket, DEAL_COMMISSION);
stats.netPL += profit;
if (reason == DEAL_REASON_TP) stats.tpHits++;
else if (reason == DEAL_REASON_SL) stats.slHits++;
else stats.otherCloses++;
if (positionId > 0 && !ContainsULong(seenPositions, seenCount, positionId))
{
ArrayResize(seenPositions, seenCount + 1);
seenPositions[seenCount++] = positionId;
// Count as "fully closed" only if the position no longer exists.
if (!PositionSelectByTicket(positionId)) stats.totalClosed++;
}
}
}
void DrawStatsPanel()
{
if (!InpShowStatsPanel) return;
VisualTradeStats stats;
ComputeTradeStats(stats);
const int xLeft = 12;
const int rowH = 18;
int y = 12;
EnsureLabel(VIS_LBL_STATS_PREFIX "pl", xLeft, y, CORNER_LEFT_LOWER,
stats.netPL >= 0 ? clrLime : clrTomato,
StringFormat("Net PL : %.2f", stats.netPL)); y += rowH;
EnsureLabel(VIS_LBL_STATS_PREFIX "other", xLeft, y, CORNER_LEFT_LOWER,
clrSilver,
StringFormat("Other : %d", stats.otherCloses)); y += rowH;
EnsureLabel(VIS_LBL_STATS_PREFIX "sl", xLeft, y, CORNER_LEFT_LOWER,
InpColorSL,
StringFormat("SL hit : %d", stats.slHits)); y += rowH;
EnsureLabel(VIS_LBL_STATS_PREFIX "tp", xLeft, y, CORNER_LEFT_LOWER,
InpColorTP,
StringFormat("TP hit : %d", stats.tpHits)); y += rowH;
EnsureLabel(VIS_LBL_STATS_PREFIX "total", xLeft, y, CORNER_LEFT_LOWER,
clrWhite,
StringFormat("Total : %d", stats.totalClosed)); y += rowH;
EnsureLabel(VIS_LBL_STATS_PREFIX "title", xLeft, y, CORNER_LEFT_LOWER,
clrWhite,
StringFormat("-- STATS (last %dd) --", InpStatsLookbackDays), 10);
}
// ------------------------------------------------------------
// Entry / SL / TP horizontal lines (TradingView style)
// ------------------------------------------------------------
void RemoveTradeLevels()
{
ObjectDelete(0, VIS_OBJ_LEVEL_PREFIX "entry");
ObjectDelete(0, VIS_OBJ_LEVEL_PREFIX "sl");
ObjectDelete(0, VIS_OBJ_LEVEL_PREFIX "tp");
}
void DrawTradeLevels()
{
if (!InpShowTradeLevels) { RemoveTradeLevels(); return; }
SignalSnapshot plan;
ZeroMemory(plan);
bool show = false;
if (g_State == STATE_PENDING_ORDER)
{
plan = g_Pending.plan;
show = true;
}
else if (g_State == STATE_IN_TRADE)
{
plan = g_OpenTrade.plan;
show = true;
}
if (!show) { RemoveTradeLevels(); return; }
const string sideStr = (plan.direction > 0) ? "BUY" : "SELL";
EnsureHLine(VIS_OBJ_LEVEL_PREFIX "entry", plan.entryPrice,
InpColorEntry, STYLE_DOT, 1,
StringFormat("%s ENTRY %.5f", sideStr, plan.entryPrice));
EnsureHLine(VIS_OBJ_LEVEL_PREFIX "sl", plan.stopLossPrice,
InpColorSL, STYLE_DASH, 1,
StringFormat("SL %.5f", plan.stopLossPrice));
EnsureHLine(VIS_OBJ_LEVEL_PREFIX "tp", plan.takeProfitPrice,
InpColorTP, STYLE_DASH, 1,
StringFormat("TP %.5f", plan.takeProfitPrice));
}
// ------------------------------------------------------------
// Indicator attach (EMA200 main + RSI cluster in subwindow)
// ------------------------------------------------------------
// Returns true if any indicator named like `prefix*` already lives on the
// given subwindow. Used to avoid stacking duplicates when the EA is reloaded.
bool IsIndicatorAlreadyAttached(const int subWindow, const string namePrefix)
{
const int total = ChartIndicatorsTotal(0, subWindow);
for (int i = 0; i < total; i++)
{
const string n = ChartIndicatorName(0, subWindow, i);
if (StringFind(n, namePrefix) == 0) return true;
}
return false;
}
void AttachIndicatorsToChart()
{
if (!InpAttachIndicators) return;
// EMA200 in main window.
if (!IsIndicatorAlreadyAttached(0, "Moving Average"))
{
if (!ChartIndicatorAdd(0, 0, g_hEMA200))
PrintFormat("[VIS] add EMA200 to main fail: %d", GetLastError());
}
// RSI cluster in a dedicated subwindow.
// We try to find an existing subwindow that already hosts an RSI; if not,
// pass CHART_WINDOWS_TOTAL to create a fresh one.
int rsiSubWindow = -1;
const int totalWindows = (int)ChartGetInteger(0, CHART_WINDOWS_TOTAL);
for (int w = 1; w < totalWindows; w++)
{
if (IsIndicatorAlreadyAttached(w, "RSI"))
{
rsiSubWindow = w;
break;
}
}
if (rsiSubWindow < 0) rsiSubWindow = totalWindows; // create new
if (!IsIndicatorAlreadyAttached(rsiSubWindow, "RSI"))
{
if (!ChartIndicatorAdd(0, rsiSubWindow, g_hRSI))
PrintFormat("[VIS] add RSI to subwindow fail: %d", GetLastError());
}
if (!IsIndicatorAlreadyAttached(rsiSubWindow, "Moving Average"))
{
if (!ChartIndicatorAdd(0, rsiSubWindow, g_hEMA9))
PrintFormat("[VIS] add RSI_EMA9 to subwindow fail: %d", GetLastError());
if (!ChartIndicatorAdd(0, rsiSubWindow, g_hWMA45))
PrintFormat("[VIS] add RSI_WMA45 to subwindow fail: %d", GetLastError());
}
ChartRedraw(0);
}
// ------------------------------------------------------------
// Top-level orchestration
// ------------------------------------------------------------
void DrawAllVisuals(const TrendDirection trendNow)
{
if (!InpVisualize) return;
DrawDashboardPanel(trendNow);
DrawStatsPanel();
DrawTradeLevels();
}
void RemoveAllVisuals()
{
ObjectsDeleteAll(0, VIS_PREFIX);
ChartRedraw(0);
}
#endif