SmartMA/SmartMA.mq5

367 lines
27 KiB
MQL5
Raw Permalink Normal View History

2025-09-14 07:00:21 -04:00
<EFBFBD><EFBFBD>//+------------------------------------------------------------------+
//| SmartMA.mq5 |
//| Copyright 2025, WebZest, LLC |
//| https://www.webzest.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, WebZest, LLC"
#property link "https://www.webzest.com"
#property version "1.00"
#property indicator_chart_window
//+------------------------------------------------------------------+
//| SmartMA MT5 - The Self-Explaining Context MA |
//| Author: Steve Rosenstock |
//| Repository: https://forge.mql5.io/steverosenstock/SmartMA |
//| Git: https://forge.mql5.io/steverosenstock/SmartMA.git |
//| Version: 1.0 (2025-07-20) |
//+------------------------------------------------------------------+
#property copyright "Steve Rosenstock"
#property link "https://forge.mql5.io/steverosenstock/SmartMA"
#property version "1.0"
#property indicator_chart_window
#property indicator_buffers 4
#property indicator_plots 3
#property indicator_label1 "SmartMA"
#property indicator_type1 DRAW_LINE
#property indicator_color1 clrDodgerBlue
#property indicator_width1 2
#property indicator_label2 "GhostMA"
#property indicator_type2 DRAW_LINE
#property indicator_color2 clrGray
#property indicator_width2 1
#property indicator_label3 "RiskBand"
#property indicator_type3 DRAW_FILLING
#property indicator_color3 clrLightYellow, clrLightYellow
#property indicator_width3 2
//--- INPUT PARAMETERS
input int MA_Period = 20; // Base MA period (auto-adapting)
input int ATR_Period = 14; // ATR for risk band
input bool Use_Volume = true; // Volume-weighted MA
input bool Use_News = true; // Integrate MT5 news events
input bool Show_Risk_Band = true; // Show SmartATR band (+/-)
input bool Show_Ghost = true; // Show "Ghost" MA (w/o news)
input bool Enable_Alerts = true; // Alert on regime/news change
//--- BUFFERS
double SmartMABuffer[]; // Main SmartMA output
double GhostMABuffer[]; // "Ghost" MA (no news)
double RiskBandTop[]; // Top of ATR band
double RiskBandBottom[]; // Bottom of ATR band
//--- GLOBALS
int lastRegime = -1;
string lastSession = "";
datetime lastExplained = 0;
double lastATR = 0;
datetime lastNewsAlert = 0;
#define CALENDAR_IMPORTANCE_LOW 1
#define CALENDAR_IMPORTANCE_MEDIUM 2
#define CALENDAR_IMPORTANCE_HIGH 3
//+------------------------------------------------------------------+
//| Initialization |
//+------------------------------------------------------------------+
int OnInit()
{
SetIndexBuffer(0, SmartMABuffer, INDICATOR_DATA);
SetIndexBuffer(1, GhostMABuffer, INDICATOR_DATA);
SetIndexBuffer(2, RiskBandTop, INDICATOR_DATA);
SetIndexBuffer(3, RiskBandBottom, INDICATOR_DATA);
PlotIndexSetString(0, PLOT_LABEL, "SmartMA");
PlotIndexSetString(1, PLOT_LABEL, "GhostMA");
PlotIndexSetString(2, PLOT_LABEL, "RiskBand");
IndicatorSetString(INDICATOR_SHORTNAME, "SmartMA MT5");
IndicatorSetInteger(INDICATOR_DIGITS, 5);
ArrayInitialize(SmartMABuffer, 0.0);
ArrayInitialize(GhostMABuffer, 0.0);
ArrayInitialize(RiskBandTop, 0.0);
ArrayInitialize(RiskBandBottom, 0.0);
Print("SmartMA MT5 initialized!");
return(INIT_SUCCEEDED);
}
//+------------------------------------------------------------------+
//| Main Calculation |
//+------------------------------------------------------------------+
int OnCalculate(const int rates_total,
const int prev_calculated,
const datetime &time[],
const double &open[],
const double &high[],
const double &low[],
const double &close[],
const long &tick_volume[],
const long &volume[],
const int &spread[])
{
if(rates_total < MathMax(MA_Period, ATR_Period) + 2)
return(0);
int currentRegime = -1; // 0=range, 1=trend, 2=neutral
string regimeText = "";
string session = "";
int dynamic_period;
double atr, vAvg;
bool isNews, highVol;
for(int i=0; i<rates_total; i++)
{
//--- 1. Detect context
session = DetectSession(time[i]);
isNews = (Use_News) ? IsEconomicNewsEvent(time[i]) : false;
highVol = (Use_Volume && tick_volume[i] > GetAvgVolume(tick_volume, i, 20) * 1.5);
atr = CalculateATR(i, ATR_Period, high, low, close);
vAvg = GetAvgVolume(tick_volume, i, MA_Period);
// Regime detection (Trend/Range)
double rollingATR = CalculateATR(i, 30, high, low, close);
if(atr > rollingATR*1.3) { regimeText="Trend"; currentRegime=1; }
else if(atr < rollingATR*0.7){ regimeText="Range"; currentRegime=0; }
else { regimeText="Neutral"; currentRegime=2; }
//--- 2. Dynamic MA period
dynamic_period = MA_Period;
if(currentRegime==1) dynamic_period = MathMax(7, MA_Period-6);
if(currentRegime==0) dynamic_period = MathMin(50, MA_Period+8);
if(session=="London") dynamic_period = MathMax(7, dynamic_period-2);
if(session=="Asia") dynamic_period = MathMin(50, dynamic_period+4);
//--- 3. MA calculation (EMA, volume-weighted if enabled)
if(isNews)
{
SmartMABuffer[i] = (i>0) ? SmartMABuffer[i-1] : close[i];
if(i>0)
GhostMABuffer[i] = GhostMABuffer[i-1];
else
GhostMABuffer[i] = close[i];
// Alert for news bar (once per bar)
if(Enable_Alerts && time[i]!=lastNewsAlert)
{
Alert("SmartMA: News event detected (", TimeToString(time[i]), ") - MA frozen");
lastNewsAlert = time[i];
}
}
else
{
SmartMABuffer[i] = AdaptiveEMA(i, dynamic_period, close, tick_volume, Use_Volume, SmartMABuffer);
GhostMABuffer[i] = AdaptiveEMA_Ghost(i, dynamic_period, close, tick_volume, Use_Volume, time, GhostMABuffer);
}
//--- 4. Risk Band (<EFBFBD>ATR)
if(Show_Risk_Band)
{
RiskBandTop[i] = SmartMABuffer[i] + atr;
RiskBandBottom[i] = SmartMABuffer[i] - atr;
}
else
{
RiskBandTop[i] = 0.0;
RiskBandBottom[i] = 0.0;
}
//--- 5. Self-Explaining Comment (latest bar only)
if(i == rates_total-1 && (time[i] != lastExplained))
{
string info = "SmartMA: " + regimeText + ", " + session;
info += ", Period=" + IntegerToString(dynamic_period);
if(isNews) info += ", NEWS EVENT (MA frozen)";
else if(highVol) info += ", High Volume";
Comment(info);
// Regime/Session change Alert
if(Enable_Alerts && (currentRegime!=lastRegime || session!=lastSession))
{
Alert("SmartMA: Context changed to " + regimeText + " / " + session + " ("+TimeToString(time[i])+")");
lastRegime = currentRegime;
lastSession = session;
}
lastExplained = time[i];
}
}
return(rates_total);
}
//+------------------------------------------------------------------+
//| Helper: Session detection |
//+------------------------------------------------------------------+
int GetEasternOffset()
{
MqlDateTime dt;
TimeToStruct(TimeGMT(), dt);
int year = dt.year;
// Find second Sunday in March
datetime march1 = StringToTime(IntegerToString(year) + ".03.01 00:00");
MqlDateTime marchDT;
TimeToStruct(march1, marchDT);
int marchDOW = marchDT.day_of_week;
int secondSundayMarch = 14 - marchDOW;
if(secondSundayMarch <= 7)
secondSundayMarch += 7;
datetime dstStart = StringToTime(IntegerToString(year) + ".03." + IntegerToString(secondSundayMarch) + " 02:00");
// Find first Sunday in November
datetime nov1 = StringToTime(IntegerToString(year) + ".11.01 00:00");
MqlDateTime novDT;
TimeToStruct(nov1, novDT);
int novDOW = novDT.day_of_week;
int firstSundayNov = (novDOW == 0) ? 1 : 8 - novDOW;
datetime dstEnd = StringToTime(IntegerToString(year) + ".11." + IntegerToString(firstSundayNov) + " 02:00");
datetime now = TimeGMT();
// Eastern Daylight Time: UTC"4
// Eastern Standard Time: UTC"5
return (now >= dstStart && now < dstEnd) ? -4 : -5;
}
//+------------------------------------------------------------------+
//| |
//+------------------------------------------------------------------+
datetime LocalTimeCurrent()
{
return TimeGMT() + GetEasternOffset() * 3600;
}
string DetectSession(datetime t)
{
MqlDateTime dt;
TimeToStruct(LocalTimeCurrent(), dt);
int hour = dt.hour;
bool isTokyo = (hour >= 19 || hour < 4); // 7 PM  4 AM ET
bool isLondon = (hour >= 3 && hour < 12); // 3 AM  12 PM ET
bool isNewYork = (hour >= 8 && hour < 17); // 8 AM  5 PM ET
bool overlapTokyoLondon = (hour >= 3 && hour < 4);
bool overlapLondonNY = (hour >= 8 && hour < 12);
bool overlapAll = (hour == 8); // 8 AM ET is triple overlap
if(overlapAll)
return "GB-NY-JP";
if(overlapTokyoLondon)
return "JP-GB";
if(overlapLondonNY)
return "GB-NY";
if(isTokyo)
return "Asia";
if(isLondon)
return "London";
if(isNewYork)
return "NewYork";
return "<<3C><19> Off Hours";
}
//+------------------------------------------------------------------+
//| Helper: News event detection (MT5 Economic Calendar) |
//+------------------------------------------------------------------+
bool IsEconomicNewsEvent(datetime bartime)
{
MqlCalendarValue news[];
datetime fromTime = bartime - 60;
datetime toTime = bartime + 60;
int count = CalendarValueHistory(news, fromTime, toTime);
if(count>0)
{
for(int i=0; i<count; i++)
{
if(news[i].impact_type >= CALENDAR_IMPORTANCE_MEDIUM)
return(true);
}
}
return(false);
}
//+------------------------------------------------------------------+
//| Helper: Rolling average volume |
//+------------------------------------------------------------------+
double GetAvgVolume(const long &volume[], int index, int window)
{
long sum = 0; int n = 0;
for(int i=index; i>index-window && i>=0; i--)
{ sum += volume[i]; n++; }
return (n>0) ? (double)sum/n : 0;
}
//+------------------------------------------------------------------+
//| Helper: ATR calculation |
//+------------------------------------------------------------------+
double CalculateATR(int i, int period, const double &high[], const double &low[], const double &close[])
{
if(i<period) return 0;
double sum = 0;
for(int j=0; j<period; j++)
{
double tr1 = high[i-j] - low[i-j];
double tr2 = MathAbs(high[i-j] - close[i-j-1]);
double tr3 = MathAbs(low[i-j] - close[i-j-1]);
double tr = MathMax(tr1, MathMax(tr2,tr3));
sum += tr;
}
return sum/period;
}
//+------------------------------------------------------------------+
//| Adaptive EMA (volume-weighted, context-sensitive) |
//+------------------------------------------------------------------+
double AdaptiveEMA(int i, int period, const double &close[], const long &volume[], bool useVolume, double &buffer[])
{
if(i==0) return close[i];
double alpha = 2.0/(period+1);
double vWeight = 1.0;
if(useVolume && volume[i]>0)
vWeight = (double)volume[i]/GetAvgVolume(volume, i, period);
return alpha*vWeight*close[i] + (1-alpha*vWeight)*buffer[i-1];
}
//+------------------------------------------------------------------+
//| Adaptive EMA for Ghost MA (skips news bars) |
//+------------------------------------------------------------------+
double AdaptiveEMA_Ghost(int i, int period, const double &close[], const long &volume[], bool useVolume, const datetime &time[], double &buffer[])
{
if(i==0) return close[i];
bool isNews = IsEconomicNewsEvent(time[i]);
if(isNews) return buffer[i-1];
double alpha = 2.0/(period+1);
double vWeight = 1.0;
if(useVolume && volume[i]>0)
vWeight = (double)volume[i]/GetAvgVolume(volume, i, period);
return alpha*vWeight*close[i] + (1-alpha*vWeight)*buffer[i-1];
}
//+------------------------------------------------------------------+
//| Developer Notes & Hooks |
//| - Community Profiles, Color Themes, Multi-Alerts: Add here! |
//+------------------------------------------------------------------+
/*
README FOR DEVELOPERS
SmartMA MT5 is a fully open, context-adaptive, self-explaining Moving Average for MetaTrader 5.
- Context-detection: Sessions, News, Regime (trend/range/neutral)
- Adaptive period and volume weighting
- "Ghost" MA for comparison (w/o news bars)
- ATR-based risk band
- Self-explaining chart overlay and alerts
Ready for profiles, color themes, regime modules, and more.
Fork, improve, and help build the next-generation Moving Average!
Repository: https://forge.mql5.io/steverosenstock/SmartMA
Git: https://forge.mql5.io/steverosenstock/SmartMA.git
MIT License
Author: Steve Rosenstock
*/