981 lines
31 KiB
Plaintext
981 lines
31 KiB
Plaintext
//+------------------------------------------------------------------+
|
|
//| Strategy.mqh - Sweep breakout multi-timeframe swing structure EA |
|
|
//| Implements trend filter on one timeframe and breakout setups on |
|
|
//| a lower signal timeframe using confirmed swing structure, ATR |
|
|
//| filtering, setup arming, and protected breakout entries. |
|
|
//+------------------------------------------------------------------+
|
|
|
|
#ifndef __STRATEGY_MQH__
|
|
#define __STRATEGY_MQH__
|
|
|
|
#include "Signal.mqh"
|
|
#include "MarketData.mqh"
|
|
#include "Logger.mqh"
|
|
#include "Config.mqh"
|
|
#include "Utilities.mqh"
|
|
|
|
// Local enums for strategy logic
|
|
enum E_TREND_STATE
|
|
{
|
|
NO_TREND = 0,
|
|
UPTREND_ACTIVE = 1,
|
|
DOWNTREND_ACTIVE = -1,
|
|
TREND_BROKEN = 2,
|
|
WAITING_FOR_NEW_TREND = 3
|
|
};
|
|
|
|
struct SSwing
|
|
{
|
|
datetime time;
|
|
double price;
|
|
bool isHigh;
|
|
ENUM_TIMEFRAMES timeframe;
|
|
ulong id;
|
|
};
|
|
|
|
struct SArmedSetup
|
|
{
|
|
bool active;
|
|
E_SIGNAL direction;
|
|
ulong setupID;
|
|
ulong trendID;
|
|
int barCountAtArm;
|
|
double triggerPrice;
|
|
bool traded;
|
|
};
|
|
|
|
class CStrategy
|
|
{
|
|
private:
|
|
CMarketData *mp_market_data;
|
|
CLogger *mp_logger;
|
|
|
|
ENUM_TIMEFRAMES m_signal_timeframe;
|
|
ENUM_TIMEFRAMES m_filter_timeframe;
|
|
|
|
int m_signal_bar_count;
|
|
int m_filter_bar_count;
|
|
|
|
int m_signal_atr_handle;
|
|
int m_filter_atr_handle;
|
|
|
|
E_TREND_STATE m_filter_state;
|
|
ulong m_filter_trend_id;
|
|
int m_trades_taken_in_current_trend;
|
|
int m_cooldown_bars_remaining;
|
|
bool m_trend_broken_on_tick;
|
|
datetime m_trend_broken_time;
|
|
|
|
SSwing m_signal_highs[];
|
|
SSwing m_signal_lows[];
|
|
SSwing m_filter_highs[];
|
|
SSwing m_filter_lows[];
|
|
ulong m_used_setup_ids[];
|
|
|
|
SArmedSetup m_buy_setup;
|
|
SArmedSetup m_sell_setup;
|
|
|
|
bool m_initialized;
|
|
|
|
public:
|
|
CStrategy(CMarketData *market_data, CLogger *logger)
|
|
{
|
|
mp_market_data = market_data;
|
|
mp_logger = logger;
|
|
|
|
m_signal_timeframe = g_strategy_entry_timeframe;
|
|
m_filter_timeframe = g_strategy_trend_timeframe;
|
|
m_signal_bar_count = 0;
|
|
m_filter_bar_count = 0;
|
|
m_signal_atr_handle = INVALID_HANDLE;
|
|
m_filter_atr_handle = INVALID_HANDLE;
|
|
m_calls = 0;
|
|
m_filter_state = NO_TREND;
|
|
m_filter_trend_id = 0;
|
|
m_trades_taken_in_current_trend = 0;
|
|
m_cooldown_bars_remaining = 0;
|
|
m_trend_broken_on_tick = false;
|
|
m_trend_broken_time = 0;
|
|
m_buy_setup.active = false;
|
|
m_sell_setup.active = false;
|
|
m_buy_setup.traded = false;
|
|
m_sell_setup.traded = false;
|
|
m_initialized = false;
|
|
}
|
|
|
|
~CStrategy()
|
|
{
|
|
Cleanup();
|
|
}
|
|
|
|
bool Init()
|
|
{
|
|
const string symbol = mp_market_data.GetSymbol();
|
|
|
|
if(g_use_atr_filter)
|
|
{
|
|
m_signal_atr_handle = iATR(symbol, m_signal_timeframe, g_atr_period);
|
|
if(m_signal_atr_handle == INVALID_HANDLE)
|
|
{
|
|
if(mp_logger)
|
|
mp_logger.Error("Failed to create ATR indicator for signal timeframe");
|
|
return false;
|
|
}
|
|
|
|
m_filter_atr_handle = iATR(symbol, m_filter_timeframe, g_atr_period);
|
|
if(m_filter_atr_handle == INVALID_HANDLE)
|
|
{
|
|
if(mp_logger)
|
|
mp_logger.Error("Failed to create ATR indicator for filter timeframe");
|
|
IndicatorRelease(m_signal_atr_handle);
|
|
m_signal_atr_handle = INVALID_HANDLE;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
m_signal_bar_count = iBars(symbol, m_signal_timeframe);
|
|
m_filter_bar_count = iBars(symbol, m_filter_timeframe);
|
|
if(m_signal_bar_count <= 0 || m_filter_bar_count <= 0)
|
|
{
|
|
if(mp_logger)
|
|
mp_logger.Error("Unable to read bars for configured timeframes");
|
|
return false;
|
|
}
|
|
|
|
ScanConfirmedSwings(m_filter_timeframe, g_filter_swing_depth, m_filter_highs, m_filter_lows);
|
|
ScanConfirmedSwings(m_signal_timeframe, g_signal_swing_depth, m_signal_highs, m_signal_lows);
|
|
UpdateFilterTrendState();
|
|
|
|
m_initialized = true;
|
|
return true;
|
|
}
|
|
|
|
E_SIGNAL GetSignal()
|
|
{
|
|
return GetTradeSetup().signal;
|
|
}
|
|
|
|
TradeSetup GetTradeSetup()
|
|
{
|
|
TradeSetup setup;
|
|
setup.signal = SIGNAL_NONE;
|
|
setup.entryPrice = 0.0;
|
|
setup.stopLoss = 0.0;
|
|
setup.takeProfit = 0.0;
|
|
setup.riskDistance = 0.0;
|
|
setup.atrValue = 0.0;
|
|
setup.reason = "No valid setup";
|
|
|
|
const string symbol = mp_market_data.GetSymbol();
|
|
m_calls++;
|
|
|
|
if(!m_initialized)
|
|
{
|
|
if(!Init())
|
|
{
|
|
setup.reason = "Strategy initialization failed";
|
|
return setup;
|
|
}
|
|
}
|
|
|
|
m_trend_broken_on_tick = false;
|
|
|
|
int filter_bar_count = iBars(symbol, m_filter_timeframe);
|
|
bool filter_new_bar = (filter_bar_count > m_filter_bar_count);
|
|
if(filter_new_bar)
|
|
m_filter_bar_count = filter_bar_count;
|
|
|
|
int signal_bar_count = iBars(symbol, m_signal_timeframe);
|
|
bool signal_new_bar = (signal_bar_count > m_signal_bar_count);
|
|
if(signal_new_bar)
|
|
m_signal_bar_count = signal_bar_count;
|
|
|
|
if(filter_new_bar)
|
|
ScanConfirmedSwings(m_filter_timeframe, g_filter_swing_depth, m_filter_highs, m_filter_lows);
|
|
if(signal_new_bar)
|
|
ScanConfirmedSwings(m_signal_timeframe, g_signal_swing_depth, m_signal_highs, m_signal_lows);
|
|
|
|
UpdateFilterTrendState();
|
|
EvaluateTrendBreak(filter_new_bar);
|
|
|
|
if(signal_new_bar)
|
|
{
|
|
ExpireSetups();
|
|
ArmSetups();
|
|
}
|
|
|
|
if(m_trend_broken_on_tick)
|
|
{
|
|
setup.reason = "Filter trend broken on current tick";
|
|
return setup;
|
|
}
|
|
|
|
if(m_filter_state != UPTREND_ACTIVE && m_filter_state != DOWNTREND_ACTIVE)
|
|
{
|
|
setup.reason = "No active filter trend";
|
|
return setup;
|
|
}
|
|
|
|
if(!mp_market_data.IsSpreadAcceptable(g_strategy_max_spread_points))
|
|
{
|
|
setup.reason = "Spread too wide for breakout entry";
|
|
return setup;
|
|
}
|
|
|
|
if(GetOpenPositionCount(symbol) >= g_strategy_max_open_positions)
|
|
{
|
|
setup.reason = "Maximum open positions achieved";
|
|
return setup;
|
|
}
|
|
|
|
if(m_buy_setup.active && m_buy_setup.direction == SIGNAL_BUY)
|
|
{
|
|
if(m_filter_state == UPTREND_ACTIVE && m_buy_setup.trendID == m_filter_trend_id && !m_buy_setup.traded)
|
|
{
|
|
if(!IsSetupExpired(m_buy_setup))
|
|
{
|
|
double ask = mp_market_data.GetAsk();
|
|
double buffer = CUtilities::PointsToPrice(symbol, g_entry_buffer_points);
|
|
double threshold = m_buy_setup.triggerPrice + (g_use_breakout_entry ? buffer : 0.0);
|
|
if(ask > threshold)
|
|
{
|
|
double stop_loss = CalculateStopLoss(symbol, SIGNAL_BUY, ask);
|
|
double take_profit = CalculateTakeProfit(symbol, SIGNAL_BUY, ask, stop_loss);
|
|
if(IsTradeLevelSetValid(SIGNAL_BUY, ask, stop_loss, take_profit))
|
|
{
|
|
setup.signal = SIGNAL_BUY;
|
|
setup.entryPrice = ask;
|
|
setup.stopLoss = CUtilities::NormalizePrice(symbol, stop_loss);
|
|
setup.takeProfit = CUtilities::NormalizePrice(symbol, take_profit);
|
|
setup.riskDistance = MathAbs(ask - stop_loss);
|
|
setup.atrValue = GetLatestATRValue(m_signal_timeframe, 1);
|
|
setup.reason = StringFormat("BUY breakout signal: FilterTrendID=%I64u SetupID=%I64u trigger=%.5f",
|
|
m_filter_trend_id, m_buy_setup.setupID, m_buy_setup.triggerPrice);
|
|
MarkSetupTraded(m_buy_setup);
|
|
return setup;
|
|
}
|
|
setup.reason = "BUY breakout rejected: invalid SL/TP";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if(m_sell_setup.active && m_sell_setup.direction == SIGNAL_SELL)
|
|
{
|
|
if(m_filter_state == DOWNTREND_ACTIVE && m_sell_setup.trendID == m_filter_trend_id && !m_sell_setup.traded)
|
|
{
|
|
if(!IsSetupExpired(m_sell_setup))
|
|
{
|
|
double bid = mp_market_data.GetBid();
|
|
double buffer = CUtilities::PointsToPrice(symbol, g_entry_buffer_points);
|
|
double threshold = m_sell_setup.triggerPrice - (g_use_breakout_entry ? buffer : 0.0);
|
|
if(bid < threshold)
|
|
{
|
|
double stop_loss = CalculateStopLoss(symbol, SIGNAL_SELL, bid);
|
|
double take_profit = CalculateTakeProfit(symbol, SIGNAL_SELL, bid, stop_loss);
|
|
if(IsTradeLevelSetValid(SIGNAL_SELL, bid, stop_loss, take_profit))
|
|
{
|
|
setup.signal = SIGNAL_SELL;
|
|
setup.entryPrice = bid;
|
|
setup.stopLoss = CUtilities::NormalizePrice(symbol, stop_loss);
|
|
setup.takeProfit = CUtilities::NormalizePrice(symbol, take_profit);
|
|
setup.riskDistance = MathAbs(stop_loss - bid);
|
|
setup.atrValue = GetLatestATRValue(m_signal_timeframe, 1);
|
|
setup.reason = StringFormat("SELL breakout signal: FilterTrendID=%I64u SetupID=%I64u trigger=%.5f",
|
|
m_filter_trend_id, m_sell_setup.setupID, m_sell_setup.triggerPrice);
|
|
MarkSetupTraded(m_sell_setup);
|
|
return setup;
|
|
}
|
|
setup.reason = "SELL breakout rejected: invalid SL/TP";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
setup.reason = "No valid breakout trigger";
|
|
return setup;
|
|
}
|
|
|
|
void LogDiagnostics()
|
|
{
|
|
if(mp_logger && g_debug_mode)
|
|
{
|
|
mp_logger.Info(StringFormat("Strategy diagnostics: calls=%I64d state=%d FilterTrendID=%I64u tradesInTrend=%d buyActive=%s sellActive=%s",
|
|
m_calls, m_filter_state, m_filter_trend_id, m_trades_taken_in_current_trend,
|
|
m_buy_setup.active ? "true" : "false",
|
|
m_sell_setup.active ? "true" : "false"));
|
|
}
|
|
}
|
|
|
|
private:
|
|
long m_calls;
|
|
|
|
void Cleanup()
|
|
{
|
|
if(m_signal_atr_handle != INVALID_HANDLE)
|
|
{
|
|
IndicatorRelease(m_signal_atr_handle);
|
|
m_signal_atr_handle = INVALID_HANDLE;
|
|
}
|
|
if(m_filter_atr_handle != INVALID_HANDLE && m_filter_atr_handle != m_signal_atr_handle)
|
|
{
|
|
IndicatorRelease(m_filter_atr_handle);
|
|
m_filter_atr_handle = INVALID_HANDLE;
|
|
}
|
|
}
|
|
|
|
double GetLatestATRValue(ENUM_TIMEFRAMES timeframe, int shift)
|
|
{
|
|
if(!g_use_atr_filter)
|
|
return 0.0;
|
|
|
|
int handle = (timeframe == m_signal_timeframe) ? m_signal_atr_handle : m_filter_atr_handle;
|
|
if(handle == INVALID_HANDLE)
|
|
return 0.0;
|
|
|
|
double value[];
|
|
ArraySetAsSeries(value, true);
|
|
ArrayResize(value, 1);
|
|
if(CopyBuffer(handle, 0, shift, 1, value) <= 0)
|
|
return 0.0;
|
|
return value[0];
|
|
}
|
|
|
|
bool ScanConfirmedSwings(ENUM_TIMEFRAMES timeframe, int depth, SSwing &highs[], SSwing &lows[])
|
|
{
|
|
const string symbol = mp_market_data.GetSymbol();
|
|
int barCount = iBars(symbol, timeframe);
|
|
if(barCount < (depth * 2 + 3))
|
|
return false;
|
|
|
|
int request = MathMin(barCount, depth * 20 + 50);
|
|
if(request < (depth * 2 + 3))
|
|
request = depth * 2 + 3;
|
|
|
|
MqlRates rates[];
|
|
ArraySetAsSeries(rates, true);
|
|
ArrayResize(rates, request);
|
|
int copied = CopyRates(symbol, timeframe, 0, request, rates);
|
|
if(copied <= 0)
|
|
return false;
|
|
ArrayResize(rates, copied);
|
|
|
|
for(int index = depth + 1; index < copied - depth; index++)
|
|
{
|
|
if(IsSwingHigh(rates, index, depth))
|
|
{
|
|
SSwing swing;
|
|
swing.time = rates[index].time;
|
|
swing.price = rates[index].high;
|
|
swing.isHigh = true;
|
|
swing.timeframe = timeframe;
|
|
swing.id = ((ulong)swing.time << 8) | (ulong)timeframe;
|
|
if(!SwingExists(swing, highs))
|
|
{
|
|
if(!IsSwingTooSmall(symbol, swing, lows))
|
|
AddSwing(highs, swing);
|
|
}
|
|
}
|
|
else if(IsSwingLow(rates, index, depth))
|
|
{
|
|
SSwing swing;
|
|
swing.time = rates[index].time;
|
|
swing.price = rates[index].low;
|
|
swing.isHigh = false;
|
|
swing.timeframe = timeframe;
|
|
swing.id = ((ulong)swing.time << 8) | (ulong)timeframe;
|
|
if(!SwingExists(swing, lows))
|
|
{
|
|
if(!IsSwingTooSmall(symbol, swing, highs))
|
|
AddSwing(lows, swing);
|
|
}
|
|
}
|
|
}
|
|
|
|
TrimSwings(highs, 64);
|
|
TrimSwings(lows, 64);
|
|
return true;
|
|
}
|
|
|
|
bool IsSwingHigh(MqlRates &rates[], int index, int depth)
|
|
{
|
|
double price = rates[index].high;
|
|
for(int offset = 1; offset <= depth; offset++)
|
|
{
|
|
if(price <= rates[index - offset].high || price <= rates[index + offset].high)
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool IsSwingLow(MqlRates &rates[], int index, int depth)
|
|
{
|
|
double price = rates[index].low;
|
|
for(int offset = 1; offset <= depth; offset++)
|
|
{
|
|
if(price >= rates[index - offset].low || price >= rates[index + offset].low)
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool SwingExists(const SSwing &swing, SSwing &array[]) const
|
|
{
|
|
for(int i = 0; i < ArraySize(array); i++)
|
|
{
|
|
if(array[i].id == swing.id)
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void AddSwing(SSwing &array[], const SSwing &swing)
|
|
{
|
|
ArrayResize(array, ArraySize(array) + 1);
|
|
array[ArraySize(array) - 1] = swing;
|
|
}
|
|
|
|
void TrimSwings(SSwing &array[], int keep)
|
|
{
|
|
int size = ArraySize(array);
|
|
if(size <= keep)
|
|
return;
|
|
int remove = size - keep;
|
|
for(int i = 0; i < keep; i++)
|
|
array[i] = array[i + remove];
|
|
ArrayResize(array, keep);
|
|
}
|
|
|
|
bool IsSwingTooSmall(const string symbol, const SSwing &swing, SSwing &oppositeSwings[])
|
|
{
|
|
if(!g_use_atr_filter)
|
|
return false;
|
|
|
|
double atrValue = GetLatestATRValue(swing.timeframe, 1);
|
|
if(atrValue <= 0.0)
|
|
return false;
|
|
|
|
double minDistance = atrValue * g_min_swing_atr_multiplier;
|
|
double lastOpposite = GetLastOppositeSwingPriceBefore(oppositeSwings, swing.time);
|
|
if(lastOpposite <= 0.0)
|
|
return false;
|
|
|
|
if(swing.isHigh)
|
|
return (swing.price - lastOpposite) < minDistance;
|
|
return (lastOpposite - swing.price) < minDistance;
|
|
}
|
|
|
|
double GetLastOppositeSwingPriceBefore(SSwing &swings[], datetime beforeTime) const
|
|
{
|
|
double price = 0.0;
|
|
for(int i = ArraySize(swings) - 1; i >= 0; i--)
|
|
{
|
|
if(swings[i].time < beforeTime)
|
|
{
|
|
price = swings[i].price;
|
|
break;
|
|
}
|
|
}
|
|
return price;
|
|
}
|
|
|
|
void UpdateFilterTrendState()
|
|
{
|
|
E_TREND_STATE detected = DetectFilterTrend();
|
|
if(detected == m_filter_state && (m_filter_state == UPTREND_ACTIVE || m_filter_state == DOWNTREND_ACTIVE))
|
|
return;
|
|
|
|
if(detected == UPTREND_ACTIVE || detected == DOWNTREND_ACTIVE)
|
|
{
|
|
if(m_filter_state == TREND_BROKEN)
|
|
return;
|
|
|
|
if(m_filter_state == WAITING_FOR_NEW_TREND && !IsFreshFilterTrendAfterBreak())
|
|
return;
|
|
|
|
if(m_filter_state != detected)
|
|
StartNewFilterTrend(detected);
|
|
}
|
|
else if(m_filter_state == TREND_BROKEN && detected != TREND_BROKEN)
|
|
{
|
|
// remain in broken/waiting until a new trend is clearly established
|
|
if(m_cooldown_bars_remaining <= 0)
|
|
m_filter_state = WAITING_FOR_NEW_TREND;
|
|
}
|
|
else if(m_filter_state == WAITING_FOR_NEW_TREND && detected == NO_TREND)
|
|
{
|
|
// remain waiting until a fresh trend forms
|
|
}
|
|
else if(m_filter_state == NO_TREND && detected == NO_TREND)
|
|
{
|
|
// no state change
|
|
}
|
|
}
|
|
|
|
void StartNewFilterTrend(E_TREND_STATE detected)
|
|
{
|
|
m_filter_trend_id++;
|
|
m_trades_taken_in_current_trend = 0;
|
|
m_cooldown_bars_remaining = 0;
|
|
m_filter_state = detected;
|
|
m_buy_setup.active = false;
|
|
m_sell_setup.active = false;
|
|
m_buy_setup.traded = false;
|
|
m_sell_setup.traded = false;
|
|
ArrayResize(m_used_setup_ids, 0);
|
|
if(mp_logger && g_debug_mode)
|
|
mp_logger.Info(StringFormat("New filter trend detected: %d, FilterTrendID=%I64u", detected, m_filter_trend_id));
|
|
}
|
|
|
|
bool IsFreshFilterTrendAfterBreak()
|
|
{
|
|
if(m_trend_broken_time <= 0)
|
|
return true;
|
|
datetime lastHighTime = GetLastSwingTime(m_filter_highs);
|
|
datetime lastLowTime = GetLastSwingTime(m_filter_lows);
|
|
return (lastHighTime > m_trend_broken_time && lastLowTime > m_trend_broken_time);
|
|
}
|
|
|
|
E_TREND_STATE DetectFilterTrend() const
|
|
{
|
|
if(ArraySize(m_filter_highs) < (g_filter_trend_confirm_count + 1) || ArraySize(m_filter_lows) < (g_filter_trend_confirm_count + 1))
|
|
return NO_TREND;
|
|
|
|
bool higherHighs = true;
|
|
for(int i = ArraySize(m_filter_highs) - g_filter_trend_confirm_count - 1; i < ArraySize(m_filter_highs) - 1; i++)
|
|
{
|
|
if(m_filter_highs[i + 1].price <= m_filter_highs[i].price)
|
|
{
|
|
higherHighs = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
bool higherLows = true;
|
|
for(int i = ArraySize(m_filter_lows) - g_filter_trend_confirm_count - 1; i < ArraySize(m_filter_lows) - 1; i++)
|
|
{
|
|
if(m_filter_lows[i + 1].price <= m_filter_lows[i].price)
|
|
{
|
|
higherLows = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(higherHighs && higherLows)
|
|
return UPTREND_ACTIVE;
|
|
|
|
bool lowerHighs = true;
|
|
for(int i = ArraySize(m_filter_highs) - g_filter_trend_confirm_count - 1; i < ArraySize(m_filter_highs) - 1; i++)
|
|
{
|
|
if(m_filter_highs[i + 1].price >= m_filter_highs[i].price)
|
|
{
|
|
lowerHighs = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
bool lowerLows = true;
|
|
for(int i = ArraySize(m_filter_lows) - g_filter_trend_confirm_count - 1; i < ArraySize(m_filter_lows) - 1; i++)
|
|
{
|
|
if(m_filter_lows[i + 1].price >= m_filter_lows[i].price)
|
|
{
|
|
lowerLows = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(lowerHighs && lowerLows)
|
|
return DOWNTREND_ACTIVE;
|
|
|
|
return NO_TREND;
|
|
}
|
|
|
|
void EvaluateTrendBreak(bool filter_new_bar)
|
|
{
|
|
if(m_filter_state != UPTREND_ACTIVE && m_filter_state != DOWNTREND_ACTIVE)
|
|
{
|
|
if(m_filter_state == TREND_BROKEN && filter_new_bar)
|
|
{
|
|
if(m_cooldown_bars_remaining > 0)
|
|
m_cooldown_bars_remaining--;
|
|
if(m_cooldown_bars_remaining <= 0)
|
|
m_filter_state = WAITING_FOR_NEW_TREND;
|
|
}
|
|
return;
|
|
}
|
|
|
|
const string symbol = mp_market_data.GetSymbol();
|
|
double buffer = CUtilities::PointsToPrice(symbol, g_break_buffer_points);
|
|
|
|
if(m_filter_state == UPTREND_ACTIVE)
|
|
{
|
|
double swingLow = GetLastSwingPrice(m_filter_lows);
|
|
if(swingLow <= 0.0)
|
|
return;
|
|
|
|
bool broken = false;
|
|
if(g_trend_break_mode == BREAK_MODE_BID_ASK_TOUCH)
|
|
broken = (mp_market_data.GetBid() <= swingLow - buffer);
|
|
else
|
|
{
|
|
double last_close = iClose(symbol, m_filter_timeframe, 1);
|
|
broken = (last_close > 0.0 && last_close <= swingLow - buffer);
|
|
}
|
|
|
|
if(broken)
|
|
{
|
|
m_filter_state = TREND_BROKEN;
|
|
m_trend_broken_on_tick = true;
|
|
m_trend_broken_time = iTime(symbol, m_filter_timeframe, 1);
|
|
if(m_trend_broken_time <= 0)
|
|
m_trend_broken_time = TimeCurrent();
|
|
m_cooldown_bars_remaining = g_cooldown_bars_after_trend_break;
|
|
CancelSetups("Filter uptrend broken");
|
|
if(mp_logger && g_debug_mode)
|
|
mp_logger.Info("Filter uptrend broken");
|
|
}
|
|
}
|
|
else if(m_filter_state == DOWNTREND_ACTIVE)
|
|
{
|
|
double swingHigh = GetLastSwingPrice(m_filter_highs);
|
|
if(swingHigh <= 0.0)
|
|
return;
|
|
|
|
bool broken = false;
|
|
if(g_trend_break_mode == BREAK_MODE_BID_ASK_TOUCH)
|
|
broken = (mp_market_data.GetAsk() >= swingHigh + buffer);
|
|
else
|
|
{
|
|
double last_close = iClose(symbol, m_filter_timeframe, 1);
|
|
broken = (last_close > 0.0 && last_close >= swingHigh + buffer);
|
|
}
|
|
|
|
if(broken)
|
|
{
|
|
m_filter_state = TREND_BROKEN;
|
|
m_trend_broken_on_tick = true;
|
|
m_trend_broken_time = iTime(symbol, m_filter_timeframe, 1);
|
|
if(m_trend_broken_time <= 0)
|
|
m_trend_broken_time = TimeCurrent();
|
|
m_cooldown_bars_remaining = g_cooldown_bars_after_trend_break;
|
|
CancelSetups("Filter downtrend broken");
|
|
if(mp_logger && g_debug_mode)
|
|
mp_logger.Info("Filter downtrend broken");
|
|
}
|
|
}
|
|
}
|
|
|
|
double GetLastSwingPrice(SSwing &swings[]) const
|
|
{
|
|
int size = ArraySize(swings);
|
|
if(size <= 0)
|
|
return 0.0;
|
|
return swings[size - 1].price;
|
|
}
|
|
|
|
void CancelSetups(const string reason)
|
|
{
|
|
if(m_buy_setup.active)
|
|
{
|
|
AddUsedSetupID(m_buy_setup.setupID);
|
|
m_buy_setup.active = false;
|
|
m_buy_setup.traded = true;
|
|
}
|
|
if(m_sell_setup.active)
|
|
{
|
|
AddUsedSetupID(m_sell_setup.setupID);
|
|
m_sell_setup.active = false;
|
|
m_sell_setup.traded = true;
|
|
}
|
|
if(mp_logger && g_debug_mode)
|
|
mp_logger.Info("Canceling setups due to trend break: " + reason);
|
|
}
|
|
|
|
void ArmSetups()
|
|
{
|
|
const string symbol = mp_market_data.GetSymbol();
|
|
if(m_trades_taken_in_current_trend >= g_max_trades_per_filter_trend)
|
|
return;
|
|
|
|
if(m_filter_state == UPTREND_ACTIVE && !m_buy_setup.active)
|
|
{
|
|
if(HasBullishSignalStructure())
|
|
{
|
|
double trigger = GetLastSwingPrice(m_signal_highs);
|
|
ulong setupID = ((ulong)GetLastSwingTime(m_signal_highs) << 8) | (ulong)m_signal_timeframe;
|
|
if(trigger > 0.0 && setupID > 0 && !IsSetupIDUsed(setupID) && mp_market_data.IsSpreadAcceptable(g_strategy_max_spread_points))
|
|
{
|
|
m_buy_setup.active = true;
|
|
m_buy_setup.direction = SIGNAL_BUY;
|
|
m_buy_setup.setupID = setupID;
|
|
m_buy_setup.trendID = m_filter_trend_id;
|
|
m_buy_setup.barCountAtArm = m_signal_bar_count;
|
|
m_buy_setup.triggerPrice = trigger;
|
|
m_buy_setup.traded = false;
|
|
if(mp_logger && g_debug_mode)
|
|
mp_logger.Info(StringFormat("Armed BUY setup: trigger=%.5f trendID=%I64u", trigger, m_filter_trend_id));
|
|
}
|
|
}
|
|
}
|
|
|
|
if(m_filter_state == DOWNTREND_ACTIVE && !m_sell_setup.active)
|
|
{
|
|
if(HasBearishSignalStructure())
|
|
{
|
|
double trigger = GetLastSwingPrice(m_signal_lows);
|
|
ulong setupID = ((ulong)GetLastSwingTime(m_signal_lows) << 8) | (ulong)m_signal_timeframe;
|
|
if(trigger > 0.0 && setupID > 0 && !IsSetupIDUsed(setupID) && mp_market_data.IsSpreadAcceptable(g_strategy_max_spread_points))
|
|
{
|
|
m_sell_setup.active = true;
|
|
m_sell_setup.direction = SIGNAL_SELL;
|
|
m_sell_setup.setupID = setupID;
|
|
m_sell_setup.trendID = m_filter_trend_id;
|
|
m_sell_setup.barCountAtArm = m_signal_bar_count;
|
|
m_sell_setup.triggerPrice = trigger;
|
|
m_sell_setup.traded = false;
|
|
if(mp_logger && g_debug_mode)
|
|
mp_logger.Info(StringFormat("Armed SELL setup: trigger=%.5f trendID=%I64u", trigger, m_filter_trend_id));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
datetime GetLastSwingTime(SSwing &swings[]) const
|
|
{
|
|
int size = ArraySize(swings);
|
|
if(size <= 0)
|
|
return 0;
|
|
return swings[size - 1].time;
|
|
}
|
|
|
|
bool HasBullishSignalStructure() const
|
|
{
|
|
return DetectSignalTrend(g_signal_trend_confirm_count) == UPTREND_ACTIVE;
|
|
}
|
|
|
|
bool HasBearishSignalStructure() const
|
|
{
|
|
return DetectSignalTrend(g_signal_trend_confirm_count) == DOWNTREND_ACTIVE;
|
|
}
|
|
|
|
E_TREND_STATE DetectSignalTrend(int confirmCount) const
|
|
{
|
|
if(ArraySize(m_signal_highs) < (confirmCount + 1) || ArraySize(m_signal_lows) < (confirmCount + 1))
|
|
return NO_TREND;
|
|
|
|
bool higherHighs = true;
|
|
for(int i = ArraySize(m_signal_highs) - confirmCount - 1; i < ArraySize(m_signal_highs) - 1; i++)
|
|
{
|
|
if(m_signal_highs[i + 1].price <= m_signal_highs[i].price)
|
|
{
|
|
higherHighs = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
bool higherLows = true;
|
|
for(int i = ArraySize(m_signal_lows) - confirmCount - 1; i < ArraySize(m_signal_lows) - 1; i++)
|
|
{
|
|
if(m_signal_lows[i + 1].price <= m_signal_lows[i].price)
|
|
{
|
|
higherLows = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(higherHighs && higherLows)
|
|
return UPTREND_ACTIVE;
|
|
|
|
bool lowerHighs = true;
|
|
for(int i = ArraySize(m_signal_highs) - confirmCount - 1; i < ArraySize(m_signal_highs) - 1; i++)
|
|
{
|
|
if(m_signal_highs[i + 1].price >= m_signal_highs[i].price)
|
|
{
|
|
lowerHighs = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
bool lowerLows = true;
|
|
for(int i = ArraySize(m_signal_lows) - confirmCount - 1; i < ArraySize(m_signal_lows) - 1; i++)
|
|
{
|
|
if(m_signal_lows[i + 1].price >= m_signal_lows[i].price)
|
|
{
|
|
lowerLows = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(lowerHighs && lowerLows)
|
|
return DOWNTREND_ACTIVE;
|
|
|
|
return NO_TREND;
|
|
}
|
|
|
|
void ExpireSetups()
|
|
{
|
|
if(m_buy_setup.active && IsSetupExpired(m_buy_setup))
|
|
{
|
|
AddUsedSetupID(m_buy_setup.setupID);
|
|
m_buy_setup.active = false;
|
|
m_buy_setup.traded = true;
|
|
if(mp_logger && g_debug_mode)
|
|
mp_logger.Info("Expired BUY setup due to age or trend change");
|
|
}
|
|
if(m_sell_setup.active && IsSetupExpired(m_sell_setup))
|
|
{
|
|
AddUsedSetupID(m_sell_setup.setupID);
|
|
m_sell_setup.active = false;
|
|
m_sell_setup.traded = true;
|
|
if(mp_logger && g_debug_mode)
|
|
mp_logger.Info("Expired SELL setup due to age or trend change");
|
|
}
|
|
}
|
|
|
|
bool IsSetupExpired(const SArmedSetup &setup) const
|
|
{
|
|
if(!setup.active)
|
|
return true;
|
|
if(setup.traded)
|
|
return true;
|
|
if(setup.trendID != m_filter_trend_id)
|
|
return true;
|
|
if(setup.direction == SIGNAL_BUY && m_filter_state != UPTREND_ACTIVE)
|
|
return true;
|
|
if(setup.direction == SIGNAL_SELL && m_filter_state != DOWNTREND_ACTIVE)
|
|
return true;
|
|
if(g_signal_expiration_bars >= 0 && (m_signal_bar_count - setup.barCountAtArm) >= g_signal_expiration_bars)
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
void MarkSetupTraded(SArmedSetup &setup)
|
|
{
|
|
AddUsedSetupID(setup.setupID);
|
|
setup.traded = true;
|
|
setup.active = false;
|
|
m_trades_taken_in_current_trend++;
|
|
}
|
|
|
|
bool IsSetupIDUsed(ulong setupID) const
|
|
{
|
|
for(int i = 0; i < ArraySize(m_used_setup_ids); i++)
|
|
{
|
|
if(m_used_setup_ids[i] == setupID)
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void AddUsedSetupID(ulong setupID)
|
|
{
|
|
if(setupID == 0 || IsSetupIDUsed(setupID))
|
|
return;
|
|
int size = ArraySize(m_used_setup_ids);
|
|
ArrayResize(m_used_setup_ids, size + 1);
|
|
m_used_setup_ids[size] = setupID;
|
|
}
|
|
|
|
int GetOpenPositionCount(const string symbol) const
|
|
{
|
|
int count = 0;
|
|
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
|
{
|
|
ulong ticket = PositionGetTicket(i);
|
|
if(ticket == 0)
|
|
continue;
|
|
if(!PositionSelectByTicket(ticket))
|
|
continue;
|
|
if(PositionGetString(POSITION_SYMBOL) == symbol && PositionGetInteger(POSITION_MAGIC) == g_magic_number)
|
|
count++;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
double CalculateStopLoss(const string symbol, E_SIGNAL direction, double entryPrice)
|
|
{
|
|
double point = CUtilities::GetPoint(symbol);
|
|
double stopLoss = 0.0;
|
|
|
|
if(g_stop_loss_mode == STOP_LOSS_STRUCTURE)
|
|
{
|
|
if(direction == SIGNAL_BUY)
|
|
{
|
|
double swingLow = GetLastSwingPrice(m_signal_lows);
|
|
if(swingLow > 0.0)
|
|
stopLoss = swingLow - g_sl_buffer_points * point;
|
|
}
|
|
else if(direction == SIGNAL_SELL)
|
|
{
|
|
double swingHigh = GetLastSwingPrice(m_signal_highs);
|
|
if(swingHigh > 0.0)
|
|
stopLoss = swingHigh + g_sl_buffer_points * point;
|
|
}
|
|
}
|
|
else if(g_stop_loss_mode == STOP_LOSS_POINTS)
|
|
{
|
|
double distance = CUtilities::PointsToPrice(symbol, g_sl_points);
|
|
stopLoss = (direction == SIGNAL_BUY) ? entryPrice - distance : entryPrice + distance;
|
|
}
|
|
else if(g_stop_loss_mode == STOP_LOSS_PERCENT)
|
|
{
|
|
double distance = MathAbs(entryPrice) * (g_sl_percent / 100.0);
|
|
stopLoss = (direction == SIGNAL_BUY) ? entryPrice - distance : entryPrice + distance;
|
|
}
|
|
|
|
stopLoss = CUtilities::NormalizePrice(symbol, stopLoss);
|
|
if(direction == SIGNAL_BUY && stopLoss >= entryPrice)
|
|
return 0.0;
|
|
if(direction == SIGNAL_SELL && stopLoss <= entryPrice)
|
|
return 0.0;
|
|
return stopLoss;
|
|
}
|
|
|
|
double CalculateTakeProfit(const string symbol, E_SIGNAL direction, double entryPrice, double stopLoss)
|
|
{
|
|
double tp = 0.0;
|
|
double riskDistance = MathAbs(entryPrice - stopLoss);
|
|
|
|
if(g_take_profit_mode == TAKE_PROFIT_POINTS)
|
|
{
|
|
double distance = CUtilities::PointsToPrice(symbol, g_tp_points);
|
|
tp = (direction == SIGNAL_BUY) ? entryPrice + distance : entryPrice - distance;
|
|
}
|
|
else if(g_take_profit_mode == TAKE_PROFIT_PERCENT)
|
|
{
|
|
double distance = MathAbs(entryPrice) * (g_tp_percent / 100.0);
|
|
tp = (direction == SIGNAL_BUY) ? entryPrice + distance : entryPrice - distance;
|
|
}
|
|
else if(g_take_profit_mode == TAKE_PROFIT_RISK_REWARD)
|
|
{
|
|
tp = (direction == SIGNAL_BUY) ? entryPrice + riskDistance * g_risk_reward_ratio : entryPrice - riskDistance * g_risk_reward_ratio;
|
|
}
|
|
|
|
tp = CUtilities::NormalizePrice(symbol, tp);
|
|
if(direction == SIGNAL_BUY && tp <= entryPrice)
|
|
return 0.0;
|
|
if(direction == SIGNAL_SELL && tp >= entryPrice)
|
|
return 0.0;
|
|
return tp;
|
|
}
|
|
|
|
bool IsTradeLevelSetValid(E_SIGNAL direction, double entryPrice, double stopLoss, double takeProfit) const
|
|
{
|
|
if(stopLoss <= 0.0)
|
|
return false;
|
|
if(direction == SIGNAL_BUY && stopLoss >= entryPrice)
|
|
return false;
|
|
if(direction == SIGNAL_SELL && stopLoss <= entryPrice)
|
|
return false;
|
|
if(takeProfit <= 0.0)
|
|
return false;
|
|
if(direction == SIGNAL_BUY && takeProfit <= entryPrice)
|
|
return false;
|
|
if(direction == SIGNAL_SELL && takeProfit >= entryPrice)
|
|
return false;
|
|
return true;
|
|
}
|
|
};
|
|
|
|
#endif //__STRATEGY_MQH__
|