Tartalom

UniPulse MQL4 Indikátor – MetaTrader 4 Forex Programozás a Gyakorlatban
Ezen az oldalon az UniPulse MQL4 indikátor teljes, éles forráskódját találod — pontosan azt a kódot, ami a MetaTrader 4 platformon fut.
Ha még nem olvastad, hogyan épül fel az indikátor mögötti logika és szűrőrendszer, az UniPulse bemutató oldalán átfogó képet kapsz róla, mielőtt belevágnál a részletekbe.
Ez az oldal a forex programozás gyakorlati oldalát mutatja be: nem csak a kész kódot közlöm, hanem lépésről lépésre, kommentezve bontom ki, hogyan gondolkodik egy forex programozó, amikor egy egyedi indikátor logikáját MQL4-ben valósítja meg — a buffer-kezeléstől a riasztási rendszerig.
Ha átugornád a programozási részt és szeretnél egyből a letöltési résznél landolni, csak kattints ide!
UniPulse.mq4 – A MetaTrader 4 Indikátor Forráskódja Letölthető Formában
Az alábbiakban megtekintheted az indikátor teljes forráskódját, amely a GitHub oldalamon is elérhető és szabadon letölthető.
Most, hogy a teljes forráskódot láttad, bontsuk ki részletesen, hogyan épül fel belülről az UniPulse.
Az alábbi szekciókban lépésről lépésre végigmegyünk a legfontosabb building blockokon — kezdve ott, ahol minden MQL4 indikátor élete kezdődik: a buffer-architektúrán.
1. A buffer-architektúra szekció
Az UniPulse összesen 13 buffert használ, amelyekből 10 jelenik meg ténylegesen a grafikonon (a #property indicator_plots limitálja a látható elemek számát).
A Fast és Slow mozgóátlagot egyaránt 3–3 külön buffer rajzolja ki (semleges, long és short szín-állapot), ami egy jól bevált trükk arra, hogy több színből álló vonalakat jelenítsünk meg a charton.
Emellett 4 buffer szolgálja a nyitó/záró nyilakat, a maradék 3 pedig (Trend, ATR, ATR_MA) INDICATOR_CALCULATIONS típusú — ezek nem jelennek meg vizuálisan, de más indikátorok vagy EA-k számára elérhetők iCustom() hívással.
A #property tulajdonságokkal állíthatjuk be az egyes plotok küllemi jellemzőit (indicator_type, indicator_color, indicator_style, indicator_width, stb).
Minden buffernek van egy sorszáma, de fontos megjegyezni, hogy itt a bufferek indexelése a programozásban megszokottaktól ellentétben 1-től kezdődik!
#property indicator_buffers 13 #property indicator_plots 10 #property indicator_type1 DRAW_LINE #property indicator_color1 clrLightGray #property indicator_style1 STYLE_DASHDOT #property indicator_width1 1 #property indicator_type2 DRAW_LINE #property indicator_color2 clrLimeGreen #property indicator_style2 STYLE_DASHDOT #property indicator_width2 1 #property indicator_type3 DRAW_LINE #property indicator_color3 clrRed #property indicator_style3 STYLE_DASHDOT #property indicator_width3 1 #property indicator_type4 DRAW_LINE #property indicator_color4 clrLightGray #property indicator_style4 STYLE_SOLID #property indicator_width4 1 #property indicator_type5 DRAW_LINE #property indicator_color5 clrLimeGreen #property indicator_style5 STYLE_SOLID #property indicator_width5 1 #property indicator_type6 DRAW_LINE #property indicator_color6 clrRed #property indicator_style6 STYLE_SOLID #property indicator_width6 1 #property indicator_type7 DRAW_ARROW #property indicator_color7 clrLimeGreen #property indicator_width7 2 #property indicator_type8 DRAW_ARROW #property indicator_color8 clrRed #property indicator_width8 2 #property indicator_type9 DRAW_ARROW #property indicator_color9 clrDodgerBlue #property indicator_width9 2 #property indicator_type10 DRAW_ARROW #property indicator_color10 clrOrange #property indicator_width10 2
2. OnInit() – Az indikátor inicializálása
Az OnInit() függvény fut le először, amikor az indikátort a grafikonra húzod, ha a charton idősíkot- vagy terméket váltasz, vagy ha bármelyik input paramétert módosítod.
Itt történik meg minden, aminek egyszer, a tényleges számítások megkezdése előtt kell lefutnia: a bufferek regisztrálása, a megjelenítési beállítások rögzítése, és — ami egy igényes egyedi indikátor programozás-nál elengedhetetlen — a bemeneti paraméterek érvényességének ellenőrzése.
2.1. Buffer-regisztráció és címkézés
Az IndicatorBuffers(13) hívás jelzi az MT4 terminálnak, hogy összesen 13 buffert fogunk használni.
A SetIndexBuffer() hívások kötik össze a globálisan deklarált tömböket a megfelelő index-pozíciókkal.
A SetIndexLabel() pedig azt határozza meg, mi jelenjen meg az adatablakban (Data Window) az egyes buffereknél névként — ez különösen fontos hibakereséskor, amikor gyorsan látni szeretnéd, melyik érték melyik állapotot (Neutral/Long/Short) reprezentálja.
A SetIndexArrow() felelős a DRAW_ARROW típusú pufferek által rajzolt jelek karakterkódjaiért (a karakterkódokat megtalálod a Wingdings kódtáblában).
Fontos megemlíteni, hogy míg a #property direktívákban 1-től, itt a pufferek indexelése már a megszokott módon, 0-tól kezdődik.
IndicatorBuffers(13); // Fast MA SetIndexBuffer(0 , fast_MA_neutral_buffer , INDICATOR_DATA ); SetIndexBuffer(1 , fast_MA_long_buffer , INDICATOR_DATA ); SetIndexBuffer(2 , fast_MA_short_buffer , INDICATOR_DATA ); SetIndexLabel(0 , "Fast MA(Neutral)" ); SetIndexLabel(1 , "Fast MA(Long)" ); SetIndexLabel(2 , "Fast MA(Short)" ); // Slow MA SetIndexBuffer(3 , slow_MA_neutral_buffer , INDICATOR_DATA ); SetIndexBuffer(4 , slow_MA_long_buffer , INDICATOR_DATA ); SetIndexBuffer(5 , slow_MA_short_buffer , INDICATOR_DATA ); SetIndexLabel(3 , "Slow MA(Neutral)" ); SetIndexLabel(4 , "Slow MA(Long)" ); SetIndexLabel(5 , "Slow MA(Short)" ); // Open/Close Arrows SetIndexBuffer(6 , long_arrow_buffer , INDICATOR_DATA ); SetIndexBuffer(7 , short_arrow_buffer , INDICATOR_DATA ); SetIndexBuffer(8 , long_close_arrow_buffer , INDICATOR_DATA ); SetIndexBuffer(9 , short_close_arrow_buffer , INDICATOR_DATA ); SetIndexLabel(6 , "Long" ); SetIndexLabel(7 , "Short" ); SetIndexLabel(8 , "Close Long" ); SetIndexLabel(9 , "Close Short" ); // Calculations SetIndexBuffer(10 , trend_state_buffer , INDICATOR_CALCULATIONS ); SetIndexBuffer(11 , ATR_buffer , INDICATOR_CALCULATIONS ); SetIndexBuffer(12 , ATR_MA_buffer , INDICATOR_CALCULATIONS ); SetIndexLabel(10 , "Trend" ); SetIndexLabel(11 , "ATR" ); SetIndexLabel(12 , "ATR_MA" ); // Arrow Codes SetIndexArrow(6 , LONG_ARROW_CODE ); SetIndexArrow(7 , SHORT_ARROW_CODE ); SetIndexArrow(8 , CLOSE_ARROW_CODE ); SetIndexArrow(9 , CLOSE_ARROW_CODE );
2.2. Bemeneti validáció
Mielőtt bármilyen számítás elindulna, öt kritikus feltételt ellenőrzünk:
- Nulla periódus a Fast-, a Slow MA-nál, az
ATR-nél, valamin aSmoothed ATR-nél — mivel mind a négyuinttípusú input, egy0érték a periódusokban a függvényeket értelmezhetetlen állapotba vinné. - Fast MA ≥ Slow MA — ha a gyors mozgóátlag periódusa eléri vagy meghaladja a lassúét, a kereszteződés-logika értelmét veszti, hiszen a „gyors” vonal többé nem reagálna gyorsabban az árváltozásra.
if(FastMaPeriod == 0){
sendCustomAlert("", "Fast MA Period is 0! The indicator stops!", true, false, false, false);
return(INIT_PARAMETERS_INCORRECT);
}
if(SlowMaPeriod == 0){
sendCustomAlert("", "Slow MA Period is 0! The indicator stops!", true, false, false, false);
return(INIT_PARAMETERS_INCORRECT);
}
if(ATRPeriod == 0){
sendCustomAlert("", "ATR Period is 0! The indicator stops!", true, false, false, false);
return(INIT_PARAMETERS_INCORRECT);
}
if(ATRSmoothingPeriod == 0){
sendCustomAlert("", "ATR Smoothing Period is 0! The indicator stops!", true, false, false, false);
return(INIT_PARAMETERS_INCORRECT);
}
if(FastMaPeriod >= SlowMaPeriod){
sendCustomAlert("", StringFormat("Fast MA Period(%d) is bigger (or equal) than Slow MA Period(%d)! The indicator stops!", FastMaPeriod, SlowMaPeriod), true, false, false, false);
return(INIT_PARAMETERS_INCORRECT);
}
Mind az öt eset ugyanazt a mintát követi: egy azonnali sendCustomAlert() hívás tájékoztatja a felhasználót a pontos okról, majd az INIT_PARAMETERS_INCORRECT visszatérési érték leállítja az indikátort.
Így soha nem fut le érvénytelen paraméterekkel, ami a forex programozás egyik alapvető jó gyakorlata: gyorsan és egyértelműen jelezni, ha valami nincs rendben, ahelyett hogy csendben hibás eredményt produkálnánk.
2.3. Számítási határ levezetése
A candles_to_calculate értéket nem közvetlenül a felhasználói input adja, hanem egy levezetett érték: a CandlesToCalculate input és a SlowMaPeriod ötszöröse közül a nagyobbik.
Ez biztosítja, hogy még ha valaki kis értéket állítana is be a „Candles To Calculate” mezőben, a mozgóátlagok akkor is elegendő historikus adaton tudjanak konvergálni, mielőtt az első jelzés megjelenne a grafikonon.
candles_to_calculate = (int)MathMax(5 * SlowMaPeriod, CandlesToCalculate);
2.4. Állapot-visszaállítás
A függvény végén minden, a kereszteződés- és jelzés-logikához tartozó állapotváltozó (last_ma_cross_direction, last_open_arrow_time, stb.) alapállapotba kerül — ez biztosítja, hogy egy újrainduló indikátor mindig „tiszta lappal” kezdje a számítást, korábbi, esetleg elavult állapot nélkül.
last_ma_cross_direction = DIRECTION_NONE; last_ma_cross_time = (datetime)0; last_open_arrow_direction = DIRECTION_NONE; last_open_arrow_time = (datetime)0; last_close_arrow_direction = DIRECTION_NONE; last_close_arrow_time = (datetime)0;
3. OnCalculate() – A fő számítási ciklus
Az OnCalculate() az UniPulse szíve — ez a függvény fut le minden egyes ártickeknél, és ez dönti el, hol jelenjen meg egy nyitó vagy záró jelzés a grafikonon.
A forex programozás szempontjából ez a rész mutatja meg igazán, miért nem elég csak „megírni” egy mozgóátlag-keresztezést: a stabil, hosszú távon megbízható működéshez körültekintő állapotkezelésre és hibavédelemre van szükség.
3.1. Bemeneti védelem és warmup-kezelés
A függvény elején egy egyszerű, de fontos ellenőrzés áll: ha kevesebb, mint 2 gyertya áll rendelkezésre, a számítás azonnal visszatér — nincs értelme mozgóátlagot vagy ATR-t számolni egyetlen gyertyán.
Ezután a kód szétválik két útvonalra: teljes újraszámítás (amikor az indikátor most kerül fel a grafikonra, vagy egy input paraméter megváltozott) és inkrementális frissítés (amikor csak az újonnan érkezett gyertyákat kell feldolgozni).
Teljes újraszámításnál minden buffer és állapotváltozó egységesen alaphelyzetbe áll, mielőtt a tényleges ciklus elindulna — ez garantálja, hogy sose maradjon „beragadt”, elavult érték egy korábbi számításból.
if(rates_total < 2){
return(rates_total); //not enough bar
}
//full chart calculation
if(prev_calculated <= 0){
candle_limit = (int)MathMin(rates_total, candles_to_calculate) - 2;
// Set default values
ArrayInitialize(fast_MA_neutral_buffer , EMPTY_VALUE );
ArrayInitialize(fast_MA_long_buffer , EMPTY_VALUE );
ArrayInitialize(fast_MA_short_buffer , EMPTY_VALUE );
ArrayInitialize(slow_MA_neutral_buffer , EMPTY_VALUE );
ArrayInitialize(slow_MA_long_buffer , EMPTY_VALUE );
ArrayInitialize(slow_MA_short_buffer , EMPTY_VALUE );
ArrayInitialize(long_arrow_buffer , EMPTY_VALUE );
ArrayInitialize(short_arrow_buffer , EMPTY_VALUE );
ArrayInitialize(long_close_arrow_buffer , EMPTY_VALUE );
ArrayInitialize(short_close_arrow_buffer , EMPTY_VALUE );
ArrayInitialize(trend_state_buffer , DIRECTION_NONE );
ArrayInitialize(ATR_buffer , EMPTY_VALUE );
ArrayInitialize(ATR_MA_buffer , EMPTY_VALUE );
last_ma_cross_direction = DIRECTION_NONE;
last_ma_cross_time = (datetime)0;
last_open_arrow_direction = DIRECTION_NONE;
last_open_arrow_time = (datetime)0;
last_close_arrow_direction = DIRECTION_NONE;
last_close_arrow_time = (datetime)0;
}else{//last closed candles calculation
candle_limit = rates_total - (int)MathMax(2, prev_calculated);
}
3.2. A ciklus logikája
A for ciklus a régebbi gyertyáktól a frissebbek felé halad — ez kulcsfontosságú, hiszen minden gyertyánál szükség van az előző gyertya Fast és Slow MA értékére a kereszteződés felismeréséhez, így a számításnak sorrendben, egymásra épülve kell történnie.
Minden iteráció elején a buffer-értékek nullázódnak, majd lekérdezésre kerül az aktuális Fast MA, Slow MA és ATR érték natív MQL4 függvényekkel (iMA, iATR), az ATR-t pedig egy második, azon futtatott mozgóátlag simítja (iMAOnArray) — ez adja a volatilitás-alapú szűrő alapját.
A figyelt gyertyához képest balra helyezkedő gyertyában tárolt Fast- és Slow MA értékeket a 3-3 pufferből nyerjük ki (amelyik pufferben tárol érték nem EMPTY_VALUE).
for(uint candle_index = candle_limit; candle_index >= 1; candle_index--){
//Reset
fast_MA_neutral_buffer[candle_index] = EMPTY_VALUE;
fast_MA_long_buffer[candle_index] = EMPTY_VALUE;
fast_MA_short_buffer[candle_index] = EMPTY_VALUE;
slow_MA_neutral_buffer[candle_index] = EMPTY_VALUE;
slow_MA_long_buffer[candle_index] = EMPTY_VALUE;
slow_MA_short_buffer[candle_index] = EMPTY_VALUE;
long_arrow_buffer[candle_index] = EMPTY_VALUE;
short_arrow_buffer[candle_index] = EMPTY_VALUE;
long_close_arrow_buffer[candle_index] = EMPTY_VALUE;
short_close_arrow_buffer[candle_index] = EMPTY_VALUE;
trend_state_buffer[candle_index] = DIRECTION_NONE;
ATR_buffer[candle_index] = EMPTY_VALUE;
ATR_MA_buffer[candle_index] = EMPTY_VALUE;
allow_alert = (candle_index == 1 && prev_calculated > 0); //last bar just closed
//------------------------------------------------------
// Get MA values for the current candle
//------------------------------------------------------
fast_ma = iMA(_Symbol, _Period, FastMaPeriod, 0, MAMethod, MAPrice, candle_index);
slow_ma = iMA(_Symbol, _Period, SlowMaPeriod, 0, MAMethod, MAPrice, candle_index);
//------------------------------------------------------
// Get MA values for the left candle
//------------------------------------------------------
prev_fast_ma = EMPTY_VALUE;
if(
NormalizeDouble(fast_MA_neutral_buffer[candle_index + 1], _Digits) != NormalizeDouble(EMPTY_VALUE, _Digits)
){
prev_fast_ma = fast_MA_neutral_buffer[candle_index + 1];
}else if(
NormalizeDouble(fast_MA_long_buffer[candle_index + 1], _Digits) != NormalizeDouble(EMPTY_VALUE, _Digits)
){
prev_fast_ma = fast_MA_long_buffer[candle_index + 1];
}else if(
NormalizeDouble(fast_MA_short_buffer[candle_index + 1], _Digits) != NormalizeDouble(EMPTY_VALUE, _Digits)
){
prev_fast_ma = fast_MA_short_buffer[candle_index + 1];
}
prev_slow_ma = EMPTY_VALUE;
if(
NormalizeDouble(slow_MA_neutral_buffer[candle_index + 1], _Digits) != NormalizeDouble(EMPTY_VALUE, _Digits)
){
prev_slow_ma = slow_MA_neutral_buffer[candle_index + 1];
}else if(
NormalizeDouble(slow_MA_long_buffer[candle_index + 1], _Digits) != NormalizeDouble(EMPTY_VALUE, _Digits)
){
prev_slow_ma = slow_MA_long_buffer[candle_index + 1];
}else if(
NormalizeDouble(slow_MA_short_buffer[candle_index + 1], _Digits) != NormalizeDouble(EMPTY_VALUE, _Digits)
){
prev_slow_ma = slow_MA_short_buffer[candle_index + 1];
}
//------------------------------------------------------
// Get ATR and fill ATR buffer
//------------------------------------------------------
atr = iATR(_Symbol, _Period, ATRPeriod, candle_index);
ATR_buffer[candle_index] = atr;
//------------------------------------------------------
// Compute Smoothed ATR
//------------------------------------------------------
atr_ma = iMAOnArray(ATR_buffer, rates_total, ATRSmoothingPeriod, 0, ATRSmoothingMethod, candle_index);
//------------------------------------------------------
// Reset direction values
//------------------------------------------------------
trend_direction = DIRECTION_NONE;
atr_direction = DIRECTION_NONE;
candle_body_direction = DIRECTION_NONE;
3.3. Adatérvényesség
Mielőtt bármilyen jelzés-logika lefutna, a kód leellenőrzi, hogy az összes bemeneti érték (Fast MA, Slow MA, előző Fast/Slow MA, ATR, simított ATR) valóban érvényes-e — ez a védelem azért kritikus, mert a historikus adat elején (amíg a mozgóátlagok még nem tudtak „felfutni”) ezek az értékek természetes módon hiányozhatnak.
Ha bármelyik érvénytelen, és ez már egy élesen futó indikátornál történik (nem a kezdeti feltöltésnél), egy figyelmeztető riasztás fut le — ez egy egyedi indikátor fejlesztésénél megszokott védőháló azon ritka esetekre, amikor a bróker vagy a terminál oldaláról érkezne hibás adat.
if(
NormalizeDouble(fast_ma, _Digits) != NormalizeDouble(EMPTY_VALUE, _Digits)
&& NormalizeDouble(slow_ma, _Digits) != NormalizeDouble(EMPTY_VALUE, _Digits)
&& NormalizeDouble(prev_fast_ma, _Digits) != NormalizeDouble(EMPTY_VALUE, _Digits)
&& NormalizeDouble(prev_slow_ma, _Digits) != NormalizeDouble(EMPTY_VALUE, _Digits)
&& NormalizeDouble(atr, 8) != NormalizeDouble(EMPTY_VALUE, 8)
&& NormalizeDouble(atr_ma, 8) != NormalizeDouble(EMPTY_VALUE, 8)
){
}else{
if(prev_calculated > 0){
sendCustomAlert(
"Invalid value!",
StringFormat(
"Invalid Value calculated! Fast MA:%s Slow MA:%s Last Fast MA:%s Last Slow MA:%s ATR:%s Smoothed ATR:%s"
, DoubleToStr(fast_ma, _Digits)
, DoubleToStr(slow_ma, _Digits)
, DoubleToStr(prev_fast_ma, _Digits)
, DoubleToStr(prev_slow_ma, _Digits)
, DoubleToStr(atr, 8)
, DoubleToStr(atr_ma, 8)
),
PopUpAlert,
PushAlert,
MailAlert
);
}
}
3.4. A jelzés-logika három rétege
Érvényes adatok esetén a ciklus három, egymásra épülő döntést hoz meg:
- Nyitó jelzés — csak akkor jelenik meg, ha a trend irány, a kereszteződés frissessége (gyertyaszám-limit), a minimális MA-távolság és — ha be van kapcsolva — a gyertyatest-irány mind egyszerre teljesülnek.
- Záró jelzés — akkor keletkezik, amikor a Fast és Slow MA visszakeresztezi egymást, és volt egy korábbi, ellenkező irányú nyitott jelzés, amit még nem zártunk le.
- Trend irány — csak akkor minősül „valós” trendnek a MA-k egymáshoz viszonyított helyzete, ha az ATR-alapú momentum-szűrő is megerősíti azt.
if(
NormalizeDouble(atr, 8) > NormalizeDouble(atr_ma, 8)
){
atr_direction = DIRECTION_LONG;
}else if(
NormalizeDouble(atr, 8) < NormalizeDouble(atr_ma, 8)
){
atr_direction = DIRECTION_SHORT;
}
//------------------------------------------------------
// Compute MA distance in point
//------------------------------------------------------
ma_distance_in_point = 0;
if(
NormalizeDouble(fast_ma, _Digits) != NormalizeDouble(slow_ma, _Digits)
){
ma_distance_in_point = (int)MathRound(MathAbs(fast_ma - slow_ma) / _Point);
}
//------------------------------------------------------
// Set current candle body direction
//------------------------------------------------------
if(CheckCandleBody){
candle_body_direction = getCandleBodyDirection(candle_index, rates_total, open, close);
}
//------------------------------------------------------
// Calculate close arrows
//------------------------------------------------------
if(
NormalizeDouble(fast_ma, _Digits) > NormalizeDouble(slow_ma, _Digits)
&& NormalizeDouble(prev_fast_ma, _Digits) <= NormalizeDouble(prev_slow_ma, _Digits)
){
if(
last_open_arrow_direction == DIRECTION_SHORT
&& (
last_close_arrow_direction != DIRECTION_SHORT
|| (
last_close_arrow_direction == DIRECTION_SHORT
&& last_open_arrow_time > 0
&& last_close_arrow_time <= last_open_arrow_time
)
)
){
last_close_arrow_direction = DIRECTION_SHORT;
last_close_arrow_time = time[candle_index];
short_close_arrow_buffer[candle_index] = high[candle_index] + getArrowShift(candle_index, rates_total, open, close);
if(allow_alert)
sendCustomAlert("Close Short", "New short closing sign.", PopUpAlert, PushAlert, MailAlert, false);
}
last_ma_cross_time = Time[candle_index];
last_ma_cross_direction = DIRECTION_LONG;
}else if(
NormalizeDouble(fast_ma, _Digits) < NormalizeDouble(slow_ma, _Digits)
&& NormalizeDouble(prev_fast_ma, _Digits) >= NormalizeDouble(prev_slow_ma, _Digits)
){
if(
last_open_arrow_direction == DIRECTION_LONG
&& (
last_close_arrow_direction != DIRECTION_LONG
|| (
last_close_arrow_direction == DIRECTION_LONG
&& last_open_arrow_time > 0
&& last_close_arrow_time <= last_open_arrow_time
)
)
){
last_close_arrow_direction = DIRECTION_LONG;
last_close_arrow_time = time[candle_index];
long_close_arrow_buffer[candle_index] = low[candle_index] - getArrowShift(candle_index, rates_total, open, close);
if(allow_alert)
sendCustomAlert("Close Long", "New long closing sign.", PopUpAlert, PushAlert, MailAlert, false);
}
last_ma_cross_time = Time[candle_index];
last_ma_cross_direction = DIRECTION_SHORT;
}
//------------------------------------------------------
// Calculate trend direction
//------------------------------------------------------
if(
NormalizeDouble(fast_ma, _Digits) > NormalizeDouble(slow_ma, _Digits)
&& atr_direction == DIRECTION_LONG
){
trend_direction = DIRECTION_LONG;
}else if(
NormalizeDouble(fast_ma, _Digits) < NormalizeDouble(slow_ma, _Digits)
&& atr_direction == DIRECTION_SHORT
){
trend_direction = DIRECTION_SHORT;
}
//------------------------------------------------------
// Calculate open arrows
//------------------------------------------------------
if(
NormalizeDouble(fast_ma, _Digits) > NormalizeDouble(slow_ma, _Digits)
){
if(
trend_direction == DIRECTION_LONG
&& last_ma_cross_direction == DIRECTION_LONG
&& last_ma_cross_time > 0
&& iBarShift(_Symbol, _Period, last_ma_cross_time) < iBarShift(_Symbol, _Period, time[candle_index]) + MinMASpreadCandleLimit
&& (
last_open_arrow_direction != DIRECTION_LONG
|| (
last_open_arrow_direction == DIRECTION_LONG
&& last_open_arrow_time < last_ma_cross_time
)
)
&& ma_distance_in_point >= MADistanceLimitPoint
&& (
!CheckCandleBody
|| (
CheckCandleBody
&& candle_body_direction == DIRECTION_LONG
)
)
){
last_open_arrow_direction = DIRECTION_LONG;
last_open_arrow_time = time[candle_index];
long_arrow_buffer[candle_index] = low[candle_index] - getArrowShift(candle_index, rates_total, open, close);
if(allow_alert)
sendCustomAlert("Open Long", "New long sign.", PopUpAlert, PushAlert, MailAlert, false);
}
}else if(
NormalizeDouble(fast_ma, _Digits) < NormalizeDouble(slow_ma, _Digits)
){
if(
trend_direction == DIRECTION_SHORT
&& last_ma_cross_direction == DIRECTION_SHORT
&& last_ma_cross_time > 0
&& iBarShift(_Symbol, _Period, last_ma_cross_time) < iBarShift(_Symbol, _Period, time[candle_index]) + MinMASpreadCandleLimit
&& (
last_open_arrow_direction != DIRECTION_SHORT
|| (
last_open_arrow_direction == DIRECTION_SHORT
&& last_open_arrow_time < last_ma_cross_time
)
)
&& ma_distance_in_point >= MADistanceLimitPoint
&& (
!CheckCandleBody
|| (
CheckCandleBody
&& candle_body_direction == DIRECTION_SHORT
)
)
){
last_open_arrow_direction = DIRECTION_SHORT;
last_open_arrow_time = time[candle_index];
short_arrow_buffer[candle_index] = high[candle_index] + getArrowShift(candle_index, rates_total, open, close);
if(allow_alert)
sendCustomAlert("Open Short", "New short sign.", PopUpAlert, PushAlert, MailAlert, false);
}
}
3.5. Buffer-feltöltés trend szerint
A ciklus végén a Fast és Slow MA értékek a trend_direction alapján a megfelelő (semleges/long/short) bufferbe kerülnek — ez az a mechanizmus, amit korábban a buffer-architektúránál bemutattunk: három külön buffer biztosítja, hogy a vonal színe pontosan kövesse a felismert trendet.
ATR_MA_buffer[candle_index] = atr_ma;
trend_state_buffer[candle_index] = trend_direction;
if(trend_direction == DIRECTION_LONG){
fast_MA_long_buffer[candle_index] = fast_ma;
fast_MA_long_buffer[candle_index + 1] = prev_fast_ma;
slow_MA_long_buffer[candle_index] = slow_ma;
slow_MA_long_buffer[candle_index + 1] = prev_slow_ma;
}
else if(trend_direction == DIRECTION_SHORT){
fast_MA_short_buffer[candle_index] = fast_ma;
fast_MA_short_buffer[candle_index + 1] = prev_fast_ma;
slow_MA_short_buffer[candle_index] = slow_ma;
slow_MA_short_buffer[candle_index + 1] = prev_slow_ma;
}
else{
fast_MA_neutral_buffer[candle_index] = fast_ma;
fast_MA_neutral_buffer[candle_index + 1] = prev_fast_ma;
slow_MA_neutral_buffer[candle_index] = slow_ma;
slow_MA_neutral_buffer[candle_index + 1] = prev_slow_ma;
}
A folyamatos vonalrajzolás érdekében a feltöltött puffereknél a balra lévő gyertyára is kiterjesztjük a rajzolást, így színváltáskor nem lesznek szakadások a színezett vonalban.
4. Segédfüggvények – Gyertyatest-irány, Dinamikus Nyíl-pozicionálás és Értesítések
4.1. getCandleBodyDirection() – A gyertyatest irányának meghatározása
Ez a függvény dönti el, hogy egy adott gyertya teste emelkedő vagy csökkenő irányú-e — ez adja az UniPulse opcionális Price Action megerősítés funkciójának alapját. Amikor a CheckCandleBody beállítás aktív, egy nyitó jelzés csak akkor jelenhet meg, ha a gyertya záró iránya megegyezik a felismert trend irányával, ez egy tipikus szűrő-elem, amit sok egyedi indikátor tartalmaz a hamis jelzések visszaszorítására.
Paraméterek:
pcandle_index– a vizsgált gyertya indexeprates_total– az elérhető gyertyák teljes száma (határellenőrzéshez)popen– a nyitóárak tömbjepclose– a záróárak tömbje
Visszatérési érték: DIRECTION_LONG, ha a záróár magasabb a nyitóárnál, DIRECTION_SHORT, ha alacsonyabb, egyébként DIRECTION_NONE.
/**
* @brief Determines the direction of a candle body based on open/close prices.
*
* @param pcandle_index Index of the candle to evaluate.
* @param prates_total Total number of available bars (for bounds checking).
* @param popen Open price array.
* @param pclose Close price array.
*
* @return DIRECTION_LONG if close > open,
* DIRECTION_SHORT if close < open,
* DIRECTION_NONE if equal, out of bounds, or price data invalid.
*/
ENUM_DIRECTION getCandleBodyDirection(const int pcandle_index, const int prates_total, const double &popen[], const double &pclose[]){
ENUM_DIRECTION direction = DIRECTION_NONE;
if(
pcandle_index >= 0
&& pcandle_index < prates_total
){
double open_price = popen[pcandle_index];
double close_price = pclose[pcandle_index];
//Valid prices
if(
NormalizeDouble(open_price, _Digits) != NormalizeDouble(EMPTY_VALUE, _Digits)
&& NormalizeDouble(close_price, _Digits) != NormalizeDouble(EMPTY_VALUE, _Digits)
){
if(
NormalizeDouble(close_price, _Digits) > NormalizeDouble(open_price, _Digits)
){//Long Body
direction = DIRECTION_LONG;
}else if(
NormalizeDouble(close_price, _Digits) < NormalizeDouble(open_price, _Digits)
){//Short Body
direction = DIRECTION_SHORT;
}
}
}
return direction;
}
4.2. getArrowShift() – Dinamikus nyíl-eltolás számítása
A grafikonon megjelenő nyilak nem fix pip- vagy pontértékkel tolódnak el a gyertyától, hanem a piac aktuális volatilitásához igazodva — ez a függvény számolja ki ezt az eltolást, a legutóbbi néhány gyertya átlagos testméretéből.
Így egy magas volatilitású instrumentumon a nyílak arányosan nagyobb távolságra kerülnek a gyertyáktól, mint egy alacsony volatilitású instrumentumon — ez egy apró, de sokat számító részlet, ami professzionálisabbá teszi a vizuális megjelenést egy forex programozó szemével nézve is.
Paraméterek:
pcandle_index– a kezdő gyertya indexe (innen néz visszafelé)prates_total– az elérhető gyertyák teljes száma (határellenőrzéshez)popen– a nyitóárak tömbjepclose– a záróárak tömbjepcalc_count– hány gyertyát vegyen figyelembe az átlagszámításnál (alapértelmezett: 20)
Visszatérési érték: a vizsgált tartomány átlagos abszolút gyertyatest-mérete, az adott szimbólum pontosságára (_Digits) normalizálva.
/**
* @brief Calculates the average candle body size starting from a given index, looking backward (older bars only).
*
* Used to derive a dynamic offset for arrow placement, scaled to recent volatility rather than a fixed point value.
*
* @param pcandle_index Starting candle index (inclusive).
* @param prates_total Total number of available bars (for bounds checking).
* @param popen Open price array.
* @param pclose Close price array.
* @param pcalc_count Total number of candles to include in the calculation (starting from pcandle_index). Default: 20.
*
* @return Average absolute body size (open-close distance) over the evaluated range, normalized to _Digits.
* Returns 0.0 if pcandle_index is out of bounds.
*/
double getArrowShift(const int pcandle_index, const int prates_total, const double &popen[], const double &pclose[], int pcalc_count = 20){
double sum = 0.0;
int count = 0;
double open_price;
double close_price;
double shift = 0.0;
if(
pcandle_index >= 0
&& pcandle_index < prates_total
){
for(int candle_index = pcandle_index; candle_index < (int)MathMin(pcandle_index + pcalc_count, prates_total); candle_index++){
open_price = popen[candle_index];
close_price = pclose[candle_index];
sum += MathAbs(open_price - close_price);
count++;
}
}
if(
count > 0
&& sum != 0.0
){
shift = NormalizeDouble(sum / count, _Digits);
}
return shift;
}
4.3. sendCustomAlert() – Kontextus-érzékeny riasztási rendszer
Ez a függvény felel az UniPulse teljes riasztás-kezeléséért: egyetlen belépési ponton keresztül tudja vezérelni a popup, push-értesítés, e-mail és naplózás (Print) csatornákat, miközben automatikusan alkalmazkodik a futási környezethez.
Éles grafikonon és Stratégia Teszterben (backtesztelés/optimalizáció) másképp viselkedik: teszt közben a push és e-mail csatornák mindig kikapcsolt állapotban vannak, hogy egy hosszú backteszt ne generáljon felesleges hálózati hívásokat vagy spam e-maileket.
Egy opcionális időalapú szűrő (throttle) pedig megakadályozza, hogy ugyanaz a riasztás túl gyakran, feleslegesen ismétlődjön — ez egy fontos jó gyakorlat minden egyedi indikátor riasztási logikájánál, hiszen egy jól működő figyelmeztető rendszer pont annyira fontos, mint maga a jelzés-generálás.
Paraméterek:
psubject– a riasztás tárgya (csak e-mailnél használt), automatikusan kiegészül a program nevével, a szimbólummal és az idősíkkalpmessage– a riasztás üzenetszövege, ugyanúgy kiegészítve, mint a tárgyppopup– megjelenjen-e natív popup ablak (Alert()); alapértelmezett: igazppush– küldjön-e push-értesítést a mobilalkalmazásra; alapértelmezett: hamispmail– küldjön-e e-mailt; alapértelmezett: hamispcheck_period– alkalmazza-e az időalapú ismétlődés-korlátozást; alapértelmezett: igaz
Visszatérési érték: nincs (a függvény közvetlenül elvégzi a kiválasztott csatornákon a küldést).
/**
* @brief Unified alert dispatcher supporting popup, push, e-mail, and log (Print) channels.
*
* Behavior adapts to the runtime context (live chart vs. strategy tester / optimization):
* - In testing/optimization, push and e-mail are always disabled; exactly one of print/popup
* fires (never both).
* - On a live chart, Print() only fires as a fallback when no other channel is active.
*
* @p pcheck_period enables a throttle (based on last_alert_time and alert_period) that prevents
* repeated alerts within a given interval.
*
* @param psubject Alert subject (used for e-mail only). Automatically prefixed with
* program name, symbol, and timeframe.
* @param pmessage Alert message body. Prefixed the same way as the subject.
* @param ppopup Show a native Alert() popup. Default: true.
* @param ppush Send a push notification (SendNotification). Errors trigger an Alert()
* popup with the MT error code/description. Default: false.
* @param pmail Send an e-mail (SendMail). Errors trigger an Alert() popup with the
* MT error code/description. Default: false.
* @param pcheck_period Apply the last_alert_time/alert_period throttle.
* Default: true.
*
* @note Relies on globals: last_alert_time, alert_period, last_error_code, last_error_message,
* and an ErrorDescription() helper.
*/
void sendCustomAlert(const string psubject, const string pmessage, bool ppopup = true, bool ppush = false, bool pmail = false, bool pcheck_period = true){
bool is_testing = IsTesting() || IsOptimization();
bool print = false;
bool popup = ppopup;
bool push = ppush;
bool mail = pmail;
datetime current_time = TimeCurrent();
string message_pre = MQLInfoString(MQL_PROGRAM_NAME) + " " + _Symbol + "_" + EnumToString((ENUM_TIMEFRAMES)_Period) + " ";
//testing mode
if(is_testing){
current_time = Time[0]; //set Time[0]
push = false; //push off
mail = false; //mail off
print = !popup; //print or popup
if(
pcheck_period
&& last_alert_time >= (current_time - alert_period)
){
print = false; //print off
popup = false; //popup off
}else{
if(pcheck_period){
last_alert_time = current_time;
}
}
}else{//real chart mode
print = !popup && !push && !mail;
if(
pcheck_period
&& last_alert_time >= (current_time - alert_period)
){
print = false;
popup = false;
push = false;
mail = false;
}else{
if(pcheck_period){
last_alert_time = current_time;
}
}
}
string subject = message_pre + psubject;
string message = message_pre + pmessage;
if(print) Print(message);
if(popup) Alert(message);
if(push){
ResetLastError();
if(!SendNotification(message)){
last_error_code = GetLastError();
last_error_message = ErrorDescription(last_error_code);
Alert(message_pre + " Push Alert Error! Message:" + message + " Error:" + last_error_message + "(" + IntegerToString(last_error_code) + ")");
}
}
if(mail){
ResetLastError();
if(!SendMail(subject, message)){
last_error_code = GetLastError();
last_error_message = ErrorDescription(last_error_code);
Alert(message_pre + " E-Mail Sending Error! Message:" + message + " Error:" + last_error_message + "(" + IntegerToString(last_error_code) + ")");
}
}
}
5. Összefoglalás és Letöltés – Az UniPulse MQL4 Indikátor Telepítése
Ezzel a teljes UniPulse MQL4 indikátor forráskódját és a mögötte húzódó logikát végigjártuk — a buffer-architektúrától kezdve, az OnInit() és OnCalculate() működésén át, egészen a segédfüggvényekig és a riasztási rendszerig. Most már csak egy lépés van hátra: az indikátor tényleges telepítése a saját MetaTrader 4 platformodra.
5.1. A csomag tartalma
- UniPulse.mq4 – a teljes, kommentezett forráskód, amit ezen az oldalon is végigjártunk
- UniPulse.ex4 – előre lefordított, futtatható verzió (ha nem szeretnéd magad lefordítani)
5.2. Telepítés lépésről lépésre
5.2.1. Ha a kész .ex4 fájlt használod:
Másold a fájlt a MetaTrader 4 adatmappájának MQL4/Indicators almappájába. Az adatmappát a terminálon belül a Fájl → Rendszermappa megnyitása menüponttal találod meg leggyorsabban — ez megnyit egy Intéző-ablakot, ahol a MQL4/Indicators mappa már ott van.
5.2.2. Ha a .mq4 forráskódot szeretnéd magad lefordítani:
Másold a UniPulse.mq4 fájlt ugyanoda, a MQL4/Indicators mappába, majd:
- Nyisd meg a MetaEditor-t (MT4-ben az Eszközök → MQL4 Szerkesztő, vagy egyszerűen
F4). - Nyisd meg benne a
UniPulse.mq4fájlt. - Nyomj
F7-et — ez lefordítja a forráskódot, és automatikusan létrehozza mellé a futtathatóUniPulse.ex4fájlt ugyanabban a mappában. - Ha a fordítás hibaüzenet nélkül, „0 error(s), 0 warning(s)” eredménnyel zárul, az indikátor készen áll a használatra.
5.2.3. Az indikátor aktiválása a grafikonon:
Indítsd újra a MetaTrader 4-et (vagy kattints jobb gombbal a Navigátor ablakban az Indikátorok mappára, és válaszd a Frissítés-t), majd húzd rá az UniPulse-t a kívánt grafikonra a Navigátor ablakból.





