2025-09-20 02:27:35 -04:00
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| SessionManager.mqh |
|
|
|
|
|
//| FR-02: Enhanced Session Filter with Gate Integration |
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
#property copyright "DualEA"
|
|
|
|
|
#property version "1.00"
|
|
|
|
|
|
|
|
|
|
// Compile-time constant for daily window slots
|
|
|
|
|
#define SESSION_MAX_WIN 4
|
|
|
|
|
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
//| Session management and filtering |
|
|
|
|
|
//+------------------------------------------------------------------+
|
|
|
|
|
class CSessionManager
|
|
|
|
|
{
|
|
|
|
|
private:
|
|
|
|
|
string m_symbol;
|
|
|
|
|
ENUM_TIMEFRAMES m_timeframe;
|
|
|
|
|
|
|
|
|
|
// Session configuration
|
|
|
|
|
bool m_enabled;
|
|
|
|
|
int m_session_start_hour;
|
|
|
|
|
int m_session_end_hour;
|
|
|
|
|
int m_max_trades_per_session;
|
|
|
|
|
double m_max_daily_loss_pct;
|
|
|
|
|
// FR-02 upgrade: timezone offset (minutes) and per-weekday windows
|
|
|
|
|
int m_tz_offset_min; // shift from server time to session local time
|
|
|
|
|
string m_windows_spec; // original spec string
|
|
|
|
|
bool m_windows_parsed; // true if weekly windows parsed successfully
|
|
|
|
|
// Windows per weekday (0=Sun..6=Sat), up to 4 windows per day
|
|
|
|
|
int m_win_count[7];
|
|
|
|
|
int m_from_min[7][SESSION_MAX_WIN]; // minutes since 00:00 local
|
|
|
|
|
int m_to_min[7][SESSION_MAX_WIN];
|
|
|
|
|
|
|
|
|
|
// Session state tracking
|
|
|
|
|
datetime m_session_start_time;
|
|
|
|
|
double m_session_baseline_equity;
|
|
|
|
|
int m_session_trade_count;
|
|
|
|
|
bool m_session_blocked;
|
|
|
|
|
|
|
|
|
|
public:
|
|
|
|
|
CSessionManager(const string symbol, const ENUM_TIMEFRAMES timeframe) :
|
|
|
|
|
m_symbol(symbol), m_timeframe(timeframe), m_enabled(false),
|
|
|
|
|
m_session_start_hour(0), m_session_end_hour(24), m_max_trades_per_session(0),
|
|
|
|
|
m_max_daily_loss_pct(0.0), m_tz_offset_min(0), m_windows_spec(""), m_windows_parsed(false),
|
|
|
|
|
m_session_start_time(0), m_session_baseline_equity(0.0),
|
|
|
|
|
m_session_trade_count(0), m_session_blocked(false)
|
|
|
|
|
{
|
|
|
|
|
for(int d2=0; d2<7; ++d2) m_win_count[d2]=0;
|
|
|
|
|
for(int d=0; d<7; ++d)
|
|
|
|
|
for(int i=0;i<SESSION_MAX_WIN;++i){ m_from_min[d][i]=0; m_to_min[d][i]=0; }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Configuration
|
|
|
|
|
void SetEnabled(const bool enabled) { m_enabled = enabled; }
|
|
|
|
|
void SetSessionHours(const int start_hour, const int end_hour)
|
|
|
|
|
{
|
|
|
|
|
m_session_start_hour = start_hour;
|
|
|
|
|
m_session_end_hour = end_hour;
|
|
|
|
|
}
|
2026-02-24 12:47:37 -05:00
|
|
|
void SetTradingHours(const int start_hour, const int end_hour)
|
|
|
|
|
{
|
|
|
|
|
SetSessionHours(start_hour, end_hour);
|
|
|
|
|
}
|
2025-09-20 02:27:35 -04:00
|
|
|
void SetMaxTradesPerSession(const int max_trades) { m_max_trades_per_session = max_trades; }
|
|
|
|
|
void SetMaxDailyLossPct(const double max_loss_pct) { m_max_daily_loss_pct = max_loss_pct; }
|
|
|
|
|
void SetTimezoneOffsetMinutes(const int tz_offset_min) { m_tz_offset_min = tz_offset_min; }
|
2026-02-24 12:47:37 -05:00
|
|
|
void SetTimezoneOffset(const int tz_offset_min) { m_tz_offset_min = tz_offset_min; }
|
2025-09-20 02:27:35 -04:00
|
|
|
void SetWeeklyWindowsString(const string spec)
|
|
|
|
|
{
|
|
|
|
|
m_windows_spec = spec;
|
|
|
|
|
m_windows_parsed = ParseWeeklyWindows(spec);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-24 12:47:37 -05:00
|
|
|
// Convenience wrapper for IsSessionAllowed
|
|
|
|
|
bool CanTrade()
|
|
|
|
|
{
|
|
|
|
|
string reason;
|
|
|
|
|
return IsSessionAllowed(reason);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Convenience wrapper for IsSessionAllowed with reason
|
|
|
|
|
bool CanTrade(string &reason)
|
|
|
|
|
{
|
|
|
|
|
return IsSessionAllowed(reason);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if currently in trading time window
|
|
|
|
|
bool IsTradingTime()
|
|
|
|
|
{
|
|
|
|
|
string reason;
|
|
|
|
|
return IsSessionAllowed(reason);
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-20 02:27:35 -04:00
|
|
|
// Session boundary detection (resets baseline/trade counters on local day change)
|
|
|
|
|
bool IsNewSession()
|
|
|
|
|
{
|
|
|
|
|
if(!m_enabled) return false;
|
|
|
|
|
datetime now = TimeCurrent();
|
|
|
|
|
datetime local_now = now + (m_tz_offset_min*60);
|
|
|
|
|
MqlDateTime dt; TimeToStruct(local_now, dt);
|
|
|
|
|
datetime current_session_start = StringToTime(StringFormat("%04d.%02d.%02d 00:00:00", dt.year, dt.mon, dt.day));
|
|
|
|
|
|
|
|
|
|
if(m_session_start_time == 0 || current_session_start > m_session_start_time)
|
|
|
|
|
{
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Initialize new session
|
|
|
|
|
void InitializeSession()
|
|
|
|
|
{
|
|
|
|
|
datetime local_now = TimeCurrent() + (m_tz_offset_min*60);
|
|
|
|
|
MqlDateTime dt; TimeToStruct(local_now, dt);
|
|
|
|
|
m_session_start_time = StringToTime(StringFormat("%04d.%02d.%02d 00:00:00", dt.year, dt.mon, dt.day));
|
|
|
|
|
m_session_baseline_equity = AccountInfoDouble(ACCOUNT_EQUITY);
|
|
|
|
|
m_session_trade_count = 0;
|
|
|
|
|
m_session_blocked = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if trading is allowed in current session
|
|
|
|
|
bool IsSessionAllowed(string &reason)
|
|
|
|
|
{
|
|
|
|
|
reason = "ok";
|
|
|
|
|
|
2026-02-24 12:47:37 -05:00
|
|
|
// Check trade count limit first - this works even if time-window checking is disabled
|
|
|
|
|
if(m_max_trades_per_session > 0 && m_session_trade_count >= m_max_trades_per_session)
|
|
|
|
|
{
|
|
|
|
|
reason = "session_trade_limit_reached";
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If not enabled, skip time-window and loss checks
|
2025-09-20 02:27:35 -04:00
|
|
|
if(!m_enabled)
|
|
|
|
|
return true;
|
|
|
|
|
|
|
|
|
|
// Check if we're in a new session
|
|
|
|
|
if(IsNewSession())
|
|
|
|
|
{
|
|
|
|
|
InitializeSession();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Determine if we are inside configured time windows
|
|
|
|
|
bool in_time_window = true; // default if no constraints
|
|
|
|
|
datetime local_now = TimeCurrent() + (m_tz_offset_min*60);
|
|
|
|
|
MqlDateTime dt; TimeToStruct(local_now, dt);
|
|
|
|
|
if(m_windows_parsed)
|
|
|
|
|
{
|
|
|
|
|
int dow = dt.day_of_week; // 0=Sun..6=Sat
|
|
|
|
|
int mins = dt.hour*60 + dt.min;
|
|
|
|
|
in_time_window = IsWithinWindows(dow, mins);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// Backward compatibility: use simple start/end hour window (local)
|
|
|
|
|
int current_hour = dt.hour;
|
|
|
|
|
if(m_session_start_hour <= m_session_end_hour)
|
|
|
|
|
in_time_window = (current_hour >= m_session_start_hour && current_hour < m_session_end_hour);
|
|
|
|
|
else
|
|
|
|
|
in_time_window = (current_hour >= m_session_start_hour || current_hour < m_session_end_hour);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(!in_time_window)
|
|
|
|
|
{
|
|
|
|
|
reason = "outside_session_hours";
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check daily loss limit
|
|
|
|
|
if(m_max_daily_loss_pct > 0.0 && m_session_baseline_equity > 0.0)
|
|
|
|
|
{
|
|
|
|
|
double current_equity = AccountInfoDouble(ACCOUNT_EQUITY);
|
|
|
|
|
double loss_pct = 100.0 * (m_session_baseline_equity - current_equity) / m_session_baseline_equity;
|
|
|
|
|
|
|
|
|
|
if(loss_pct > m_max_daily_loss_pct)
|
|
|
|
|
{
|
|
|
|
|
reason = StringFormat("daily_loss_limit_exceeded_%.2f_pct", loss_pct);
|
|
|
|
|
m_session_blocked = true;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if session was previously blocked
|
|
|
|
|
if(m_session_blocked)
|
|
|
|
|
{
|
|
|
|
|
reason = "session_blocked";
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Record a new trade
|
|
|
|
|
void RecordTrade()
|
|
|
|
|
{
|
2026-02-24 12:47:37 -05:00
|
|
|
// Always count trades - max trades limit should work even if time-window checking is disabled
|
|
|
|
|
m_session_trade_count++;
|
2025-09-20 02:27:35 -04:00
|
|
|
}
|
|
|
|
|
|
2026-02-24 12:47:37 -05:00
|
|
|
// Get current trade count
|
|
|
|
|
int GetTradeCount() const { return m_session_trade_count; }
|
|
|
|
|
|
2025-09-20 02:27:35 -04:00
|
|
|
// Get session statistics
|
|
|
|
|
void GetSessionStats(int &trade_count, double &pnl_pct, bool &is_blocked)
|
|
|
|
|
{
|
|
|
|
|
trade_count = m_session_trade_count;
|
|
|
|
|
is_blocked = m_session_blocked;
|
|
|
|
|
|
|
|
|
|
if(m_session_baseline_equity > 0.0)
|
|
|
|
|
{
|
|
|
|
|
double current_equity = AccountInfoDouble(ACCOUNT_EQUITY);
|
|
|
|
|
pnl_pct = 100.0 * (current_equity - m_session_baseline_equity) / m_session_baseline_equity;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
pnl_pct = 0.0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Reset session (for testing or manual reset)
|
|
|
|
|
void ResetSession()
|
|
|
|
|
{
|
|
|
|
|
m_session_start_time = 0;
|
|
|
|
|
m_session_baseline_equity = 0.0;
|
|
|
|
|
m_session_trade_count = 0;
|
|
|
|
|
m_session_blocked = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
int DayAbbrevToIndex(const string d)
|
|
|
|
|
{
|
|
|
|
|
string s=d; StringToLower(s);
|
|
|
|
|
if(StringFind(s, "sun")==0) return 0;
|
|
|
|
|
if(StringFind(s, "mon")==0) return 1;
|
|
|
|
|
if(StringFind(s, "tue")==0) return 2;
|
|
|
|
|
if(StringFind(s, "wed")==0) return 3;
|
|
|
|
|
if(StringFind(s, "thu")==0) return 4;
|
|
|
|
|
if(StringFind(s, "fri")==0) return 5;
|
|
|
|
|
if(StringFind(s, "sat")==0) return 6;
|
|
|
|
|
return -1;
|
|
|
|
|
}
|
|
|
|
|
bool ParseHHMM(const string hhmm, int &mins)
|
|
|
|
|
{
|
|
|
|
|
int pos = StringFind(hhmm, ":");
|
|
|
|
|
if(pos<0) return false;
|
|
|
|
|
string hh = StringSubstr(hhmm, 0, pos);
|
|
|
|
|
string mm = StringSubstr(hhmm, pos+1);
|
|
|
|
|
int h = (int)StringToInteger(hh);
|
|
|
|
|
int m = (int)StringToInteger(mm);
|
|
|
|
|
if(h<0||h>23||m<0||m>59) return false;
|
|
|
|
|
mins = h*60 + m;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
bool ParseRange(const string range, int &from_min, int &to_min)
|
|
|
|
|
{
|
|
|
|
|
int pos = StringFind(range, "-");
|
|
|
|
|
if(pos<0) return false;
|
|
|
|
|
string a = StringSubstr(range, 0, pos);
|
|
|
|
|
string b = StringSubstr(range, pos+1);
|
|
|
|
|
int fm=0, tm=0;
|
|
|
|
|
if(!ParseHHMM(a, fm)) return false;
|
|
|
|
|
if(!ParseHHMM(b, tm)) return false;
|
|
|
|
|
from_min=fm; to_min=tm; return true;
|
|
|
|
|
}
|
|
|
|
|
bool ParseWeeklyWindows(const string spec)
|
|
|
|
|
{
|
|
|
|
|
// reset counts and windows
|
|
|
|
|
for(int d=0; d<7; ++d)
|
|
|
|
|
{
|
|
|
|
|
m_win_count[d]=0;
|
|
|
|
|
for(int i=0;i<SESSION_MAX_WIN;++i){ m_from_min[d][i]=0; m_to_min[d][i]=0; }
|
|
|
|
|
}
|
|
|
|
|
if(spec=="" || StringLen(spec)<3) return false;
|
|
|
|
|
// format example: "Mon=09:30-16:00;Tue=09:30-16:00;Sat=*;Sun=*"
|
|
|
|
|
int start=0;
|
|
|
|
|
while(start < StringLen(spec))
|
|
|
|
|
{
|
|
|
|
|
int sep = StringFind(spec, ";", start);
|
|
|
|
|
string token = (sep<0? StringSubstr(spec, start) : StringSubstr(spec, start, sep-start));
|
|
|
|
|
if(token!="")
|
|
|
|
|
{
|
|
|
|
|
int eq = StringFind(token, "=");
|
|
|
|
|
if(eq>0)
|
|
|
|
|
{
|
|
|
|
|
string day = StringSubstr(token, 0, eq);
|
|
|
|
|
string val = StringSubstr(token, eq+1);
|
|
|
|
|
int di = DayAbbrevToIndex(day);
|
|
|
|
|
if(di>=0)
|
|
|
|
|
{
|
|
|
|
|
if(StringLen(val)>0 && val!="*")
|
|
|
|
|
{
|
|
|
|
|
// multiple ranges supported via comma
|
|
|
|
|
int st=0;
|
|
|
|
|
while(st < StringLen(val) && m_win_count[di] < SESSION_MAX_WIN)
|
|
|
|
|
{
|
|
|
|
|
int comma = StringFind(val, ",", st);
|
|
|
|
|
string r = (comma<0? StringSubstr(val, st) : StringSubstr(val, st, comma-st));
|
|
|
|
|
int fm=0, tm=0;
|
|
|
|
|
if(ParseRange(r, fm, tm))
|
|
|
|
|
{
|
|
|
|
|
int idx = m_win_count[di];
|
|
|
|
|
m_from_min[di][idx]=fm; m_to_min[di][idx]=tm; m_win_count[di]++;
|
|
|
|
|
}
|
|
|
|
|
if(comma<0) break; else st = comma+1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// '*' means closed all day -> count=0 already
|
|
|
|
|
m_win_count[di]=0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if(sep<0) break; else start = sep+1;
|
|
|
|
|
}
|
|
|
|
|
// success if at least one day has windows
|
|
|
|
|
for(int d=0; d<7; ++d) if(m_win_count[d]>0) return true;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
bool IsWithinWindows(const int dow, const int minutes_local)
|
|
|
|
|
{
|
|
|
|
|
int d = (dow%7);
|
|
|
|
|
int n = m_win_count[d];
|
|
|
|
|
if(n<=0) return false; // closed day
|
|
|
|
|
for(int i=0;i<n;++i)
|
|
|
|
|
{
|
|
|
|
|
int a = m_from_min[d][i];
|
|
|
|
|
int b = m_to_min[d][i];
|
|
|
|
|
if(a<=b)
|
|
|
|
|
{
|
|
|
|
|
if(minutes_local>=a && minutes_local<b) return true;
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
// overnight window within same day definition (e.g., 22:00-02:00)
|
|
|
|
|
if(minutes_local>=a || minutes_local<b) return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
};
|