| //+------------------------------------------------------------------+
|
| //| SessionClassificationEngine.mq5 |
|
| //| Intraday Session Classification Engine | roc need recalculation
|
| //+------------------------------------------------------------------+ #288 Ranginging needs calculation, make room for undefined.
|
| #property copyright "Session Classification EA"
|
| #property version "1.00"
|
| #property strict
|
|
|
| #include <Trade\Trade.mqh>
|
|
|
| //--- Inputs: Session 1
|
| input group "=== Session 1 ==="
|
| input string InpS1Name = "Asia";
|
| input ENUM_TIMEFRAMES InpS1TF = PERIOD_M15;
|
| input int InpS1StartHour = 0;
|
| input int InpS1StartMin = 0;
|
| input int InpS1EndHour = 7;
|
| input int InpS1EndMin = 0;
|
|
|
| //--- Inputs: Session 2
|
| input group "=== Session 2 ==="
|
| input string InpS2Name = "London";
|
| input ENUM_TIMEFRAMES InpS2TF = PERIOD_M15;
|
| input int InpS2StartHour = 7;
|
| input int InpS2StartMin = 0;
|
| input int InpS2EndHour = 12;
|
| input int InpS2EndMin = 0;
|
|
|
| //--- Inputs: Session 3
|
| input group "=== Session 3 ==="
|
| input string InpS3Name = "NewYork";
|
| input ENUM_TIMEFRAMES InpS3TF = PERIOD_M15;
|
| input int InpS3StartHour = 12;
|
| input int InpS3StartMin = 0;
|
| input int InpS3EndHour = 17;
|
| input int InpS3EndMin = 0;
|
|
|
| //--- Inputs: Classification Thresholds
|
| input group "=== Classification Thresholds ==="
|
| double InpVolatileCompressionMax = 0.7; // Compression < X → potentially volatile
|
| input double InpVolatileSpanMultiplier = 1.0; // Span > avg * X → volatile
|
| double InpTrendCompressionMin = 0.65; // Compression > X → trending
|
| double InpTrendSlopeMin = 0.00005; // |slope| > X → trending
|
| input int InpAvgSpanLookbackDays = 20; // Days for historical avg span
|
|
|
| //--- Inputs: Visuals
|
| input group "=== Visuals ==="
|
| input color InpColorTrending = clrDodgerBlue;
|
| input color InpColorRanging = clrGoldenrod;
|
| input color InpColorVolatile = clrCrimson;
|
| input int InpFillAlpha = 40;
|
| input int InpFontSize = 9;
|
| input string InpFontName = "Arial Black";
|
|
|
| //--- Structure
|
| struct SessionDef
|
| {
|
| string name;
|
| ENUM_TIMEFRAMES tf;
|
| int startHour;
|
| int startMin;
|
| int endHour;
|
| int endMin;
|
| };
|
|
|
| struct SessionStats
|
| {
|
| double highestHigh;
|
| double lowestLow;
|
| double bodyHigh; // highest of max(open,close)
|
| double bodyLow; // lowest of min(open,close)
|
| double sumBodies;
|
| double slope;
|
| double roc; // rate of change (close-end vs close-start)
|
| double span;
|
| double compression;
|
| string classification;
|
| };
|
|
|
| //--- Globals
|
| SessionDef g_sessions[3];
|
| datetime g_lastProcessedDay = 0;
|
| int g_avgSpanSamples = 0;
|
| double g_avgSpanSum = 0.0;
|
| double g_avgSpan = 0.0;
|
| int g_rectCounter = 0;
|
|
|
| //+------------------------------------------------------------------+
|
| int OnInit()
|
| {
|
| g_sessions[0].name = InpS1Name;
|
| g_sessions[0].tf = InpS1TF;
|
| g_sessions[0].startHour = InpS1StartHour;
|
| g_sessions[0].startMin = InpS1StartMin;
|
| g_sessions[0].endHour = InpS1EndHour;
|
| g_sessions[0].endMin = InpS1EndMin;
|
|
|
| g_sessions[1].name = InpS2Name;
|
| g_sessions[1].tf = InpS2TF;
|
| g_sessions[1].startHour = InpS2StartHour;
|
| g_sessions[1].startMin = InpS2StartMin;
|
| g_sessions[1].endHour = InpS2EndHour;
|
| g_sessions[1].endMin = InpS2EndMin;
|
|
|
| g_sessions[2].name = InpS3Name;
|
| g_sessions[2].tf = InpS3TF;
|
| g_sessions[2].startHour = InpS3StartHour;
|
| g_sessions[2].startMin = InpS3StartMin;
|
| g_sessions[2].endHour = InpS3EndHour;
|
| g_sessions[2].endMin = InpS3EndMin;
|
|
|
| Print("Session Classification EA initialized.");
|
| return(INIT_SUCCEEDED);
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| void OnDeinit(const int reason)
|
| {
|
| ObjectsDeleteAll(0, "SCE_");
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| void OnTick()
|
| {
|
| datetime now = TimeCurrent();
|
| MqlDateTime dt;
|
| TimeToStruct(now, dt);
|
|
|
| // Build today's midnight
|
| MqlDateTime mid = dt;
|
| mid.hour = 0; mid.min = 0; mid.sec = 0;
|
| datetime todayMidnight = StructToTime(mid);
|
|
|
| // New day detection
|
| if(todayMidnight != g_lastProcessedDay)
|
| {
|
| g_lastProcessedDay = todayMidnight;
|
| ProcessNewDay(todayMidnight);
|
| }
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Handle a new day: classify yesterday's sessions |
|
| //+------------------------------------------------------------------+
|
| void ProcessNewDay(datetime todayMidnight)
|
| {
|
| // We analyze the PREVIOUS day's completed sessions
|
| datetime yesterdayMidnight = todayMidnight - 86400;
|
|
|
| // Clear previous drawings
|
| ObjectsDeleteAll(0, "SCE_");
|
| g_rectCounter = 0;
|
|
|
| double daySpanTotal = 0.0;
|
|
|
| for(int i = 0; i < 3; i++)
|
| {
|
| datetime sStart = yesterdayMidnight + g_sessions[i].startHour * 3600
|
| + g_sessions[i].startMin * 60;
|
| datetime sEnd = yesterdayMidnight + g_sessions[i].endHour * 3600
|
| + g_sessions[i].endMin * 60;
|
|
|
| SessionStats st;
|
| if(!ComputeSession(g_sessions[i].tf, sStart, sEnd, st))
|
| {
|
| PrintFormat("Session %s: no data.", g_sessions[i].name);
|
| continue;
|
| }
|
|
|
| // Average span tracking (all session spans across days)
|
| g_avgSpanSum += st.span;
|
| g_avgSpanSamples++;
|
| if(g_avgSpanSamples > 0)
|
| g_avgSpan = g_avgSpanSum / g_avgSpanSamples;
|
|
|
| // Classify
|
| st.classification = ClassifySession(st, g_avgSpan);
|
|
|
| // Draw
|
| DrawSession(sStart, sEnd, st, i);
|
|
|
| daySpanTotal += st.span;
|
|
|
| printf("avgSpanSum: %f, avgSpanSamples: %i, avgSpan: %f, totalSpanDays: %f", g_avgSpanSum, g_avgSpanSamples, g_avgSpan, daySpanTotal);
|
|
|
| PrintFormat("[%s] %s | H=%.5f L=%.5f BodyH=%.5f BodyL=%.5f Slope=%.5f ROC=%.5f Comp=%.3f Span=%.5f Class=%s",
|
| g_sessions[i].name,
|
| TimeToString(sStart, TIME_DATE|TIME_MINUTES),
|
| st.highestHigh, st.lowestLow, st.bodyHigh, st.bodyLow,
|
| st.slope, st.roc, st.compression, st.span, st.classification);
|
| }
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Compute session stats from bars |
|
| //+------------------------------------------------------------------+
|
| bool ComputeSession(ENUM_TIMEFRAMES tf, datetime sStart, datetime sEnd, SessionStats &st)
|
| {
|
| st.highestHigh = -DBL_MAX;
|
| st.lowestLow = DBL_MAX;
|
| st.bodyHigh = -DBL_MAX;
|
| st.bodyLow = DBL_MAX;
|
| st.sumBodies = 0.0;
|
| st.slope = 0.0;
|
| st.roc = 0.0;
|
| st.span = 0.0;
|
| st.compression = 0.0;
|
| st.classification = "RANGING";
|
|
|
| MqlRates rates[];
|
| int copied = CopyRates(_Symbol, tf, sStart, sEnd, rates);
|
| if(copied <= 1)
|
| return false;
|
|
|
| int n = copied;
|
| double closes[];
|
| ArrayResize(closes, n);
|
|
|
| for(int i = 0; i < n; i++)
|
| {
|
| if(rates[i].high > st.highestHigh) st.highestHigh = rates[i].high;
|
| if(rates[i].low < st.lowestLow) st.lowestLow = rates[i].low;
|
| printf("rangeHigh: %f, rangeLow: %f", st.highestHigh, st.lowestLow); //corrrect
|
|
|
| double bHi = MathMax(rates[i].open, rates[i].close);
|
| double bLo = MathMin(rates[i].open, rates[i].close);
|
| if(bHi > st.bodyHigh) st.bodyHigh = bHi;
|
| if(bLo < st.bodyLow) st.bodyLow = bLo;
|
| printf("bodyHigh: %f, bodyLow: %f", st.bodyHigh, st.bodyLow); //corrrect
|
|
|
| st.sumBodies = st.bodyHigh - st.bodyLow;
|
| printf("st.bodyHeight: %f", st.sumBodies); //corrrect
|
|
|
| closes[i] = rates[i].close;
|
| }
|
|
|
| st.span = st.highestHigh - st.lowestLow;
|
| printf("st.rangeHeight: %f", st.span); //corrrect
|
|
|
| if(st.span <= 0.0)
|
| {
|
| st.classification = "RANGING";
|
| return true;
|
| }
|
|
|
| st.compression = st.sumBodies / st.span;
|
| printf("compression: %f", st.compression);
|
|
|
| // Linear regression slope over closes
|
| st.slope = LinearRegressionSlope(closes, n);
|
|
|
|
|
| // Rate of change: (last close - first close) / first close
|
| if(closes[0] != 0.0)
|
| st.roc = (closes[n-1] - closes[0]) / closes[0];
|
| else
|
| st.roc = 0.0;
|
|
|
| return true;
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Standard linear regression slope |
|
| //+------------------------------------------------------------------+
|
| double LinearRegressionSlope(const double &y[], int n)
|
| {
|
| if(n < 2) return 0.0;
|
|
|
| double sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0;
|
| for(int i = 0; i < n; i++)
|
| {
|
| double x = (double)i;
|
| sumX += x;
|
| sumY += y[i];
|
| sumXY += x * y[i];
|
| sumX2 += x * x;
|
| }
|
| double denom = n * sumX2 - sumX * sumX;
|
| if(MathAbs(denom) < 1e-12) return 0.0;
|
| double slope = (n * sumXY - sumX * sumY) / denom;
|
| printf("slope: %f", slope);
|
| return slope;
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Classification matrix per spec |
|
| //+------------------------------------------------------------------+
|
| string ClassifySession(const SessionStats &st, double avgSpan)
|
| {
|
| // Edge case: no movement
|
| if(st.span == 0.0)
|
| return "RANGING";
|
|
|
| // Condition A: VOLATILE
|
| if(st.compression < InpVolatileCompressionMax &&
|
| avgSpan > 0.0 &&
|
| st.span > avgSpan * InpVolatileSpanMultiplier)
|
| {
|
| return "VOLATILE";
|
| }
|
|
|
| // Condition B: TRENDING
|
| if(st.compression > InpTrendCompressionMin &&
|
| MathAbs(st.slope) > InpTrendSlopeMin)
|
| {
|
| return "TRENDING";
|
| }
|
|
|
| // Condition C: RANGING (default)
|
| return "RANGING";
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Draw rectangle + text label |
|
| //+------------------------------------------------------------------+
|
| void DrawSession(datetime sStart, datetime sEnd, const SessionStats &st, int idx)
|
| {
|
| color baseCol = InpColorRanging;
|
| if(st.classification == "TRENDING") baseCol = InpColorTrending;
|
| else if(st.classification == "VOLATILE") baseCol = InpColorVolatile;
|
|
|
| string rname = StringFormat("SCE_RECT_%d_%d", idx, (int)sStart);
|
| string tname = StringFormat("SCE_TEXT_%d_%d", idx, (int)sStart);
|
|
|
| datetime t1 = sStart;
|
| datetime t2 = sEnd;
|
| double p1 = st.lowestLow;
|
| double p2 = st.highestHigh;
|
|
|
| if(!ObjectCreate(0, rname, OBJ_RECTANGLE, 0, t1, p1, t2, p2))
|
| {
|
| PrintFormat("ObjectCreate rectangle failed: %d", GetLastError());
|
| return;
|
| }
|
| ObjectSetInteger(0, rname, OBJPROP_COLOR, baseCol);
|
| ObjectSetInteger(0, rname, OBJPROP_STYLE, STYLE_SOLID);
|
| ObjectSetInteger(0, rname, OBJPROP_WIDTH, 1);
|
| ObjectSetInteger(0, rname, OBJPROP_FILL, true);
|
| ObjectSetInteger(0, rname, OBJPROP_BACK, true);
|
| ObjectSetInteger(0, rname, OBJPROP_SELECTABLE, false);
|
| ObjectSetInteger(0, rname, OBJPROP_HIDDEN, true);
|
|
|
| // Text label just below rectangle
|
| double labelPrice = p1 - (st.span * 0.05);
|
| if(st.span == 0.0)
|
| labelPrice = p1 - _Point * 50;
|
|
|
| string labelText = StringFormat("%s: %s", g_sessions[idx].name, st.classification);
|
|
|
| if(!ObjectCreate(0, tname, OBJ_TEXT, 0, t1, labelPrice))
|
| {
|
| PrintFormat("ObjectCreate text failed: %d", GetLastError());
|
| return;
|
| }
|
| ObjectSetString(0, tname, OBJPROP_TEXT, labelText);
|
| ObjectSetString(0, tname, OBJPROP_FONT, InpFontName);
|
| ObjectSetInteger(0, tname, OBJPROP_FONTSIZE, InpFontSize);
|
| ObjectSetInteger(0, tname, OBJPROP_COLOR, baseCol);
|
| ObjectSetInteger(0, tname, OBJPROP_ANCHOR, ANCHOR_LEFT_UPPER);
|
| ObjectSetInteger(0, tname, OBJPROP_SELECTABLE, false);
|
| ObjectSetInteger(0, tname, OBJPROP_HIDDEN, true);
|
|
|
| g_rectCounter++;
|
| }
|
| //+------------------------------------------------------------------+ |