Add RSI EA
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"path": ".."
|
||||
},
|
||||
{
|
||||
"path": "D:/Coding Tools"
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"files.associations": {
|
||||
"*.mqh": "cpp",
|
||||
"*.mq4": "cpp",
|
||||
"*.mq5": "cpp"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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.).
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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';
|
||||
|
||||
@@ -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]))
|
||||
|
||||
@@ -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
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
Binary file not shown.
Reference in New Issue
Block a user