Add RSI EA

This commit is contained in:
Bell
2026-05-09 23:16:50 +07:00
parent 95de84e1a2
commit 5e015de79f
16 changed files with 2041 additions and 15 deletions
+17
View File
@@ -0,0 +1,17 @@
{
"folders": [
{
"path": ".."
},
{
"path": "D:/Coding Tools"
}
],
"settings": {
"files.associations": {
"*.mqh": "cpp",
"*.mq4": "cpp",
"*.mq5": "cpp"
}
}
}
+159
View File
@@ -0,0 +1,159 @@
//+------------------------------------------------------------------+
//| 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
@@ -0,0 +1,74 @@
#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
@@ -0,0 +1,122 @@
#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
+235
View File
@@ -0,0 +1,235 @@
# RSIForceStateEA
EA giao dich pullback theo dong luc RSI, loc trend bang EMA200, dieu phoi qua state
machine 4 trang thai. Chi dung 1 lenh moi pullback, vao bang BUY/SELL LIMIT.
## 1) Cau truc thu muc
- EA chinh: `Experts/RSIForceStateEA.mq5`
- Cac module:
- `Experts/RSIForceStateEA/Config.mqh` - toan bo input
- `Experts/RSIForceStateEA/State.mqh` - enum + struct (no globals)
- `Experts/RSIForceStateEA/Indicators.mqh` - handle + buffer + helper slope/cross
- `Experts/RSIForceStateEA/Trade.mqh` - dat lenh, sizing, partial + BE
- `Experts/RSIForceStateEA/StateMachine.mqh` - flow 4 state
- `Experts/RSIForceStateEA/Visualizer.mqh` - dashboard, stats panel, trade levels
- `Experts/RSIForceStateEA/README.md`
Thu tu 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 tong the
### 2.1 Cac chi bao
- `RSI(InpRSIPeriod)` - dong luc gia.
- `EMA(InpRSI_EMA9Period)` tinh tren RSI.
- `WMA(InpRSI_WMA45Period)` tinh tren RSI.
- `EMA(InpEMA200Period)` tren close - loc trend.
- `ATR(InpATRPeriod)` - dung cho trend buffer va SL.
### 2.2 Trend filter
- `Uptrend` khi `Close > EMA200 + buffer`.
- `Downtrend` khi `Close < EMA200 - buffer`.
- `buffer`:
- `% EMA200` mac dinh (`InpTrendBufferPercent = 0.10%`), hoac
- `ATR-based` neu `InpUseATRTrendBuffer = true`.
- `InpSkipFlatEMA200`: bo qua khi EMA200 phang (delta giua 2 nen <= `ATR * InpFlatEMA_ATRMult`).
### 2.3 4 trang thai
| State | Y nghia |
| --------------------- | -------------------------------------------------- |
| `STATE_NO_TRADE` | khong co setup hop le |
| `STATE_WATCHING` | da co pullback hop le, dang cho trigger |
| `STATE_PENDING_ORDER` | da dat BUY/SELL LIMIT, dang cho khop |
| `STATE_IN_TRADE` | da khop, dang quan ly position (partial + BE + TP) |
### 2.4 Pullback (NO_TRADE -> WATCHING)
- Uptrend pullback:
- `RSI < EMA9 < WMA45`
- 3 buffer cung slope giam (kiem tra `InpSlopeLookbackBars` nen)
- Downtrend pullback (mirror).
### 2.5 Trigger entry (WATCHING -> PENDING_ORDER)
- BUY trigger:
- RSI cat len WMA45 (so sanh nen `InpSignalBarShift` voi nen truoc).
- EMA9 van con < WMA45 (xac nhan dong luc moi bat).
- Khong co cross RSI/WMA45 trong `InpMinBarsBetweenCrosses` nen truoc do
(tin hieu phai "isolated", tranh nhieu sat WMA45).
- SELL trigger (mirror).
### 2.6 Dat LIMIT ORDER
- `Entry = (Close(signal) + SwingExtreme) / 2`
- BUY: SwingExtreme = swing low gan nhat trong `InpSwingLookbackBars`.
- SELL: SwingExtreme = swing high gan nhat.
- Lenh dat la `BuyLimit` / `SellLimit` (khong co market chase).
- Pending song toi da `InpPendingMaxAliveBars`. Het han -> huy.
- Neu `InpInvalidateIfCrossBack = true`, khi RSI cross nguoc lai WMA45
hoac trend mat -> huy ngay.
- WATCHING song toi da `InpWatchingMaxBars`. Het han -> tro ve NO_TRADE.
### 2.7 Stop Loss
| Mode | Cong thuc |
| ----------- | -------------------------------------------------------------- |
| `SL_SWING` | swing extreme +/- `InpSL_SwingBufferPoints` |
| `SL_ATR` | `Entry +/- ATR * InpSL_ATRMult` |
| `SL_HYBRID` | chon SL **rong hon** giua swing va ATR (an toan hon, mac dinh) |
### 2.8 Take Profit
- `TP = Entry +/- R * InpRiskRewardRatio` (mac dinh `2R`).
### 2.9 Quan ly trong IN_TRADE
- Risk = `InpRiskPercent` (% balance, mac dinh 1%).
- Khi gia chay duoc `InpPartialCloseAtR` (mac dinh `1.5R`):
- Dong `InpPartialClosePercent`% volume (mac dinh 50%).
- Doi SL ve `Entry` (BE).
- Neu volume khong the chia (volMin chan), van dich SL ve BE va bo qua partial.
- Khong lam gi them o `1R`.
### 2.10 Anti-spam
- Mot `pullback` chi sinh ra 1 lenh (`g_HasTradedThisPullback`).
- Reset bookkeeping khi trend regime doi (TREND_UP <-> TREND_DOWN/NONE).
- Khi trend doi va dang co pending nguoc huong -> huy luon.
### 2.11 Sideway filter
- `InpUseRSISidewayFilter`: khi tat ca `InpSidewayLookbackBars` gia tri RSI gan nhat
nam trong `[InpSidewayRSILow, InpSidewayRSIHigh]` -> bo qua tim setup moi.
- Filter nay chi chan `NO_TRADE -> WATCHING`, khong chan vong doi pending va
khong chan quan ly position.
## 3) Cach build va chay
1. Mo MetaEditor.
2. Mo `MQL5/Experts/RSIForceStateEA.mq5` va Compile (F7).
3. Quay lai MT5, attach EA vao chart muon trade.
4. Bat `Algo Trading`.
5. Inputs co the dieu chinh ngay tu UI khi attach.
## 4) Cac input quan trong (mac dinh)
| Input | Mac dinh | Mo ta |
| -------------------------- | --------- | --------------------------------------- |
| `InpMagicNumber` | 26050901 | Magic, doi neu chay nhieu instance |
| `InpRiskPercent` | 1.0 | % balance/lenh |
| `InpRiskRewardRatio` | 2.0 | TP = R * day |
| `InpPartialCloseAtR` | 1.5 | dong 1 phan tai R nay |
| `InpPartialClosePercent` | 50.0 | % volume dong tai partial |
| `InpPendingMaxAliveBars` | 5 | huy pending sau N nen |
| `InpWatchingMaxBars` | 10 | huy WATCHING neu khong trigger |
| `InpMinBarsBetweenCrosses` | 10 | dam bao tin hieu cross "isolated" |
| `InpSlopeLookbackBars` | 3 | so nen kiem tra slope |
| `InpSwingLookbackBars` | 20 | tim swing extreme cho entry/SL |
| `InpStopLossMode` | SL_HYBRID | SL_SWING / SL_ATR / SL_HYBRID |
| `InpSL_ATRMult` | 1.2 | ATR multiplier khi SL theo ATR |
| `InpSL_SwingBufferPoints` | 20 | them buffer (point) ngoai swing extreme |
| `InpUseRSISidewayFilter` | true | bat/tat sideway filter |
| `InpSidewayRSILow / High` | 45 / 55 | dai sideway theo RSI |
| `InpUseATRTrendBuffer` | false | doi trend buffer sang dang ATR |
## 5) Goi y test va toi uu
- Backtest tung symbol toi thieu 6-12 thang truoc khi chay live.
- Voi index/forex bien dong cao -> bat `InpUseATRTrendBuffer = true`.
- Muon **it tin hieu hon nhung chat hon**:
- tang `InpMinBarsBetweenCrosses`
- tang `InpSlopeLookbackBars`
- giu `SL_HYBRID`
- Muon **nhieu tin hieu**:
- giam `InpSlopeLookbackBars` ve 2
- tat `InpUseRSISidewayFilter`
- Theo doi tab `Experts` / `Journal` de xem log `[STATE]`, `[TREND]`, `[PENDING]`,
`[TRADE]`.
## 6) Visualization
EA tu hien thi dau day du de quan sat va danh gia:
### 6.1 Dashboard (goc tren-trai)
Hien thi cac dong:
- **Trend** : UP / DOWN / NONE (mau xanh / do / xam)
- **State** : `STATE_NO_TRADE` / `STATE_WATCHING` / `STATE_PENDING_ORDER` / `STATE_IN_TRADE`
- **RSI** : gia tri RSI, EMA9, WMA45 cua nen tin hieu (`InpSignalBarShift`)
- **EMA200**: gia tri EMA200 + close hien tai
- **ATR** : gia tri ATR
- **Context**: tuy state se in:
- `Watching: x/N bars`
- `Pending : dir entry alive=x/N`
- `Trade : dir entry partial=DONE/PEND`
### 6.2 Stats panel (goc duoi-trai)
Quet history `InpStatsLookbackDays` ngay (mac dinh 60), filter theo
`InpMagicNumber` + `_Symbol`, hien thi:
- `Total` : so position da dong (theo POSITION_ID, dedupe partial)
- `TP hit` : so deal dong voi reason `DEAL_REASON_TP`
- `SL hit` : so deal dong voi reason `DEAL_REASON_SL`
- `Other` : dong thu cong / partial close / expert close
- `Net PL` : tong P/L (profit + swap + commission)
### 6.3 Trade levels (TradingView style)
Khi state = `PENDING_ORDER` hoac `IN_TRADE`, ve 3 line ngang:
- `ENTRY` (dotted, `InpColorEntry`)
- `SL` (dashed, `InpColorSL`)
- `TP` (dashed, `InpColorTP`)
Tu dong xoa khi quay ve `NO_TRADE`.
### 6.4 Auto-attach indicators
Khi `InpAttachIndicators = true` (mac dinh):
- `EMA200` duoc them vao **main chart**.
- `RSI(14)`, `RSI_EMA9`, `RSI_WMA45` duoc them vao **subwindow moi**.
Co the tat tung phan rieng:
| Input | Mac dinh | Tac dung |
| ---------------------- | -------- | ------------------------------------- |
| `InpVisualize` | true | bat tat toan bo overlay |
| `InpAttachIndicators` | true | tu add EMA200 / RSI cluster vao chart |
| `InpShowDashboard` | true | panel goc tren-trai |
| `InpShowStatsPanel` | true | panel goc duoi-trai |
| `InpShowTradeLevels` | true | line Entry/SL/TP |
| `InpStatsLookbackDays` | 60 | so ngay quet stats |
Tat ca object visual dung prefix `RSIForce`_ va duoc xoa o `OnDeinit`.
## 7) Ghi chu kien truc
- Toan bo state machine xoay quanh **closed bar** (`IsNewBar()`), tranh fire
nhieu lan trong cung 1 nen.
- Quan ly position (partial + BE) chay **moi tick** de phan ung nhanh khi
gia di chuyen.
- `SyncStateWithBroker()` chay moi tick + sau moi `OnTradeTransaction` -
dam bao state nha minh luon khop voi broker (truong hop user dong tay,
pending bi reject, v.v.).
+52
View File
@@ -0,0 +1,52 @@
#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
@@ -0,0 +1,371 @@
#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
@@ -0,0 +1,328 @@
#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
@@ -0,0 +1,366 @@
#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
+18 -9
View File
@@ -14,21 +14,30 @@ input int InpEMAFastPeriod = 34;
input int InpEMASlowPeriod = 89;
//--- Step 2: FVG Detection ---
input int InpFVGLookbackBars = 100;
input int InpFVGMaxAgeBars = 50;
input double InpFVGMinBodyPct = 50.0;
input double InpFVGMinSizePoints = 0;
input double InpFVGTouchedPercent = 33.0;
input double InpFVGMinGapVsImpulsePct = 30.0;
input int InpFVGLookbackBars = 120;
input int InpFVGMaxAgeBars = 36;
input double InpFVGMinBodyPct = 55.0;
input double InpFVGMinSizePoints = 120;
input double InpFVGTouchedPercent = 35.0;
input double InpFVGMinGapVsImpulsePct = 35.0;
input double InpFVGMaxOuterBarRatio = 2.0;
//--- Step 3: Trading ---
input bool InpTradeEnabled = true;
input double InpRiskPercentPerR = 1.0;
input double InpRiskPercentPerR = 1;
input double InpRRRatio = 2.2;
input int InpMaxLimitOrders = 3;
input int InpLimitMaxAgeBars = 24;
input int InpMaxLimitOrders = 1;
input int InpLimitMaxAgeBars = 12;
input long InpEAMagic = 123456;
input int InpMaxSpreadPoints = 350; // XAUUSD-friendly default
input bool InpUseSessionFilter = true;
input int InpSessionStartHour = 7; // server time hour [0..23]
input int InpSessionEndHour = 23; // server time hour [0..23], supports overnight window
input bool InpUseATRFilter = true;
input int InpATRPeriod = 14;
input double InpMinATRPoints = 1200; // skip low-volatility regime
input double InpMaxATRPoints = 7000; // skip extreme-volatility regime
input double InpLowTFEntryRangeBufferPoints = 60; // HTF-LTF mapping tolerance
//--- Step 4: Drawing ---
input color InpColorBullFVG = C'30,80,140';
+2
View File
@@ -228,6 +228,8 @@ void DrawFVGZones()
stateStr = " [TOUCHED]";
else if(IsZoneMitigated(g_FVGZones[i]))
stateStr = " [MITIGATED]";
if(g_FVGZones[i].tradeLocked)
stateStr += " [LOCKED]";
color labelColor;
if(IsZoneMitigated(g_FVGZones[i]))
+100
View File
@@ -29,6 +29,9 @@ struct FVGZone
double slReferencePrice;
datetime createdTime;
int ageInBars;
bool tradeLocked; // true after one order attempt is placed from this zone
ulong linkedOrderTicket;
datetime tradeLockedTime;
};
//+------------------------------------------------------------------+
@@ -302,6 +305,103 @@ bool GetLatestLowTFFVG(string symbol,
return false;
}
//+------------------------------------------------------------------+
//| Low-TF FVG nearest and aligned with HTF zone price range |
//+------------------------------------------------------------------+
bool GetLatestLowTFFVGInRange(string symbol,
ENUM_TIMEFRAMES tf,
ENUM_FVG_TYPE type,
int maxLookbackBars,
double rangeLower,
double rangeUpper,
double rangeBufferPoints,
double &outUpper,
double &outLower,
double &outBarALow,
double &outBarAHigh)
{
int totalBars = Bars(symbol, tf);
int maxShift = MathMin(maxLookbackBars, totalBars - 3);
if(maxShift < 1)
return false;
double maxOuterRatio = InpFVGMaxOuterBarRatio;
double minGapVsBody = InpFVGMinGapVsImpulsePct / 100.0;
double bufferPrice = rangeBufferPoints * _Point;
double minAllowed = rangeLower - bufferPrice;
double maxAllowed = rangeUpper + bufferPrice;
for(int shift = 1; shift <= maxShift; shift++)
{
int shiftA = shift + 2;
int shiftB = shift + 1;
int shiftC = shift;
double candleA_High = iHigh(symbol, tf, shiftA);
double candleA_Low = iLow (symbol, tf, shiftA);
double candleB_High = iHigh(symbol, tf, shiftB);
double candleB_Low = iLow (symbol, tf, shiftB);
double candleB_Open = iOpen (symbol, tf, shiftB);
double candleB_Close = iClose(symbol, tf, shiftB);
double candleC_High = iHigh(symbol, tf, shiftC);
double candleC_Low = iLow (symbol, tf, shiftC);
double rangeA = candleA_High - candleA_Low;
double rangeB = candleB_High - candleB_Low;
double rangeC = candleC_High - candleC_Low;
double bodyB = MathAbs(candleB_Close - candleB_Open);
if(rangeB <= 0 || bodyB <= 0)
continue;
if(rangeA > maxOuterRatio * rangeB || rangeC > maxOuterRatio * rangeB)
continue;
if(!IsImpulseCandleStrong(symbol, tf, shiftB))
continue;
if(type == FVG_BULLISH)
{
if(candleA_High >= candleC_Low || candleB_Close <= candleB_Open)
continue;
double gap = candleC_Low - candleA_High;
double gapRatio = gap / bodyB;
if(gapRatio < minGapVsBody)
continue;
double entry = candleC_Low;
if(entry < minAllowed || entry > maxAllowed)
continue;
outUpper = candleC_Low;
outLower = candleA_High;
outBarALow = candleA_Low;
outBarAHigh = candleA_High;
return true;
}
else
{
if(candleA_Low <= candleC_High || candleB_Close >= candleB_Open)
continue;
double gap = candleA_Low - candleC_High;
double gapRatio = gap / bodyB;
if(gapRatio < minGapVsBody)
continue;
double entry = candleC_High;
if(entry < minAllowed || entry > maxAllowed)
continue;
outUpper = candleA_Low;
outLower = candleC_High;
outBarALow = candleA_Low;
outBarAHigh = candleA_High;
return true;
}
}
return false;
}
//+------------------------------------------------------------------+
//| Check if price has mitigated (filled through) any active FVG |
//+------------------------------------------------------------------+
+196 -5
View File
@@ -13,6 +13,135 @@
//+------------------------------------------------------------------+
//| Internal helpers |
//+------------------------------------------------------------------+
string FVGTypeToString(ENUM_FVG_TYPE type)
{
return (type == FVG_BULLISH) ? "BULL" : "BEAR";
}
void LogDecisionTrace(const string stage, const string message)
{
if(!InpDebugLog)
return;
PrintFormat("[TRACE][%s] %s", stage, message);
}
bool IsSpreadAcceptable(string symbol)
{
if(InpMaxSpreadPoints <= 0)
return true;
double ask = SymbolInfoDouble(symbol, SYMBOL_ASK);
double bid = SymbolInfoDouble(symbol, SYMBOL_BID);
double spreadPoints = -1.0;
if(ask > 0.0 && bid > 0.0)
spreadPoints = (ask - bid) / _Point;
else
{
// In Strategy Tester, BID/ASK can be unavailable on some bars.
// Fallback to broker/tester spread setting (already in points).
long spreadInt = SymbolInfoInteger(symbol, SYMBOL_SPREAD);
if(spreadInt > 0)
spreadPoints = (double)spreadInt;
}
if(spreadPoints < 0.0)
return false;
return (spreadPoints <= InpMaxSpreadPoints);
}
double GetCurrentSpreadPoints(string symbol)
{
double ask = SymbolInfoDouble(symbol, SYMBOL_ASK);
double bid = SymbolInfoDouble(symbol, SYMBOL_BID);
if(ask > 0.0 && bid > 0.0)
return (ask - bid) / _Point;
long spreadInt = SymbolInfoInteger(symbol, SYMBOL_SPREAD);
if(spreadInt > 0)
return (double)spreadInt;
return -1.0;
}
bool IsTradingSessionOpen()
{
if(!InpUseSessionFilter)
return true;
datetime now = TimeCurrent();
if(now == 0)
return false;
MqlDateTime dt;
TimeToStruct(now, dt);
int hour = dt.hour;
int startHour = MathMax(0, MathMin(23, InpSessionStartHour));
int endHour = MathMax(0, MathMin(23, InpSessionEndHour));
if(startHour == endHour)
return true;
if(startHour < endHour)
return (hour >= startHour && hour < endHour);
return (hour >= startHour || hour < endHour); // overnight session
}
int GetCurrentServerHour()
{
datetime now = TimeCurrent();
if(now == 0)
return -1;
MqlDateTime dt;
TimeToStruct(now, dt);
return dt.hour;
}
void GetNormalizedSessionHours(int &outStartHour, int &outEndHour)
{
outStartHour = MathMax(0, MathMin(23, InpSessionStartHour));
outEndHour = MathMax(0, MathMin(23, InpSessionEndHour));
}
double GetATRPoints(string symbol, ENUM_TIMEFRAMES tf, int period)
{
if(period <= 1)
return -1.0;
int handle = iATR(symbol, tf, period);
if(handle == INVALID_HANDLE)
return -1.0;
double buffer[1];
int copied = CopyBuffer(handle, 0, 1, 1, buffer);
IndicatorRelease(handle);
if(copied < 1 || buffer[0] <= 0.0)
return -1.0;
return buffer[0] / _Point;
}
bool IsATRRegimeValid(string symbol)
{
if(!InpUseATRFilter)
return true;
double atrPoints = GetATRPoints(symbol, InpTimeframe, InpATRPeriod);
if(atrPoints <= 0.0)
return false;
if(InpMinATRPoints > 0.0 && atrPoints < InpMinATRPoints)
return false;
if(InpMaxATRPoints > 0.0 && atrPoints > InpMaxATRPoints)
return false;
return true;
}
double GetCurrentATRPoints(string symbol)
{
return GetATRPoints(symbol, InpTimeframe, InpATRPeriod);
}
int CountOurPositions()
{
int count = 0;
@@ -382,9 +511,35 @@ void ManageFVGTrades()
if(currentLimits >= InpMaxLimitOrders)
return;
string symbol = GetTradeSymbol();
if(!IsSpreadAcceptable(symbol))
{
double spreadPts = GetCurrentSpreadPoints(symbol);
LogDecisionTrace("FILTER", StringFormat("Skip entries: spread %.1f > max %d points",
spreadPts, InpMaxSpreadPoints));
return;
}
if(!IsTradingSessionOpen())
{
int startHour, endHour;
GetNormalizedSessionHours(startHour, endHour);
int currentHour = GetCurrentServerHour();
LogDecisionTrace("FILTER", StringFormat("Skip entries: session closed (hour=%d, window=%02d-%02d, useSession=%s)",
currentHour, startHour, endHour,
InpUseSessionFilter ? "true" : "false"));
return;
}
if(!IsATRRegimeValid(symbol))
{
double atrPoints = GetCurrentATRPoints(symbol);
LogDecisionTrace("FILTER", StringFormat("Skip entries: ATR %.1f outside [%.1f..%.1f] points (period=%d, useATR=%s)",
atrPoints, InpMinATRPoints, InpMaxATRPoints,
InpATRPeriod, InpUseATRFilter ? "true" : "false"));
return;
}
// Chỉ tìm tín hiệu low TF FVG khi high TF FVG đã TOUCHED (giá lấp đủ %), không trigger khi mới chạm cạnh
ENUM_TREND_DIRECTION trend = g_CurrentTrend;
string symbol = GetTradeSymbol();
ENUM_TIMEFRAMES lowTF = GetConfirmationTimeframe(InpTimeframe);
const int LOW_TF_FVG_LOOKBACK = 15;
@@ -392,25 +547,52 @@ void ManageFVGTrades()
{
FVGZone zone = g_FVGZones[i];
if(!IsZoneActive(zone) || IsZoneMitigated(zone))
{
LogDecisionTrace("ZONE", StringFormat("#%d skip: inactive/mitigated", i));
continue;
}
if(zone.tradeLocked)
{
LogDecisionTrace("ZONE", StringFormat("#%d %s skip: locked", i, FVGTypeToString(zone.type)));
continue;
}
if(zone.type == FVG_BULLISH && trend != TREND_BULLISH)
{
LogDecisionTrace("ZONE", StringFormat("#%d BULL blocked by EMA trend", i));
continue;
}
if(zone.type == FVG_BEARISH && trend != TREND_BEARISH)
{
LogDecisionTrace("ZONE", StringFormat("#%d BEAR blocked by EMA trend", i));
continue;
}
// Điều kiện vào lệnh: FVG high TF phải đã TOUCHED (giá lấp >= InpFVGTouchedPercent), không chỉ chạm cạnh
if(!IsZoneTouched(zone))
{
LogDecisionTrace("ZONE", StringFormat("#%d %s not touched", i, FVGTypeToString(zone.type)));
continue;
}
// Chỉ đặt lệnh khi có low TF để xác nhận (H1->M5, H4->M15, M15->M2)
if(lowTF == InpTimeframe)
{
LogDecisionTrace("ZONE", StringFormat("#%d skip: lowTF mapping unavailable", i));
continue;
}
double ltfUpper, ltfLower, ltfBarALow, ltfBarAHigh;
if(!GetLatestLowTFFVG(symbol, lowTF, zone.type, LOW_TF_FVG_LOOKBACK,
ltfUpper, ltfLower, ltfBarALow, ltfBarAHigh))
if(!GetLatestLowTFFVGInRange(symbol, lowTF, zone.type, LOW_TF_FVG_LOOKBACK,
zone.lowerEdge, zone.upperEdge,
InpLowTFEntryRangeBufferPoints,
ltfUpper, ltfLower, ltfBarALow, ltfBarAHigh))
{
LogDecisionTrace("ZONE", StringFormat("#%d %s no LTF FVG aligned to HTF zone [%.5f..%.5f]",
i, FVGTypeToString(zone.type), zone.lowerEdge, zone.upperEdge));
continue;
}
// Entry theo low TF FVG; SL = bar B của high TF FVG
double entryPrice, slPrice;
@@ -427,10 +609,19 @@ void ManageFVGTrades()
if(PlaceLimitFromLowTF(symbol, zone.type, entryPrice, slPrice))
{
// Mỗi FVG chỉ được dùng để trade 1 lần
g_FVGZones[i].status = EXPIRED;
// v2 state: lock zone after first successful placement; keep visible for diagnostics.
g_FVGZones[i].tradeLocked = true;
g_FVGZones[i].tradeLockedTime = TimeCurrent();
g_FVGZones[i].linkedOrderTicket = 0;
LogDecisionTrace("ORDER", StringFormat("#%d %s placed @%.5f SL=%.5f HTF[%.5f..%.5f] LTF[%.5f..%.5f]",
i, FVGTypeToString(zone.type), entryPrice, slPrice,
zone.lowerEdge, zone.upperEdge, ltfLower, ltfUpper));
currentLimits++;
}
else
{
LogDecisionTrace("ORDER", StringFormat("#%d %s failed @%.5f", i, FVGTypeToString(zone.type), entryPrice));
}
}
}
+1 -1
View File
@@ -204,7 +204,7 @@ bool CFilePipe::ReadInteger(T &value)
if(WaitForRead(sizeof(T)))
{
ResetLastError();
value=FileReadInteger(m_handle,sizeof(T));
value=(T)FileReadInteger(m_handle,sizeof(T));
return(GetLastError()==0);
}
//--- failure
Binary file not shown.
BIN
View File
Binary file not shown.