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