| //+------------------------------------------------------------------+
|
| //| GoldTrendSentinel.mqh |
|
| //| Copyright 2026, Allan Niñal |
|
| //| https://www.allanninal.dev/mql5 |
|
| //+------------------------------------------------------------------+
|
| //| Gold Trend Sentinel MT4: a gold H4 trend follower. |
|
| //| On each new H4 bar, from the last closed one: |
|
| //| buy when the close crosses above the UT Bot stop line |
|
| //| (key 1.5, ATR 10) AND the last completed daily close is |
|
| //| above its EMA(200) AND Supertrend(10, 3) on H4 is up; |
|
| //| sell is the mirror. Market order at the bar's open; stop |
|
| //| 3 x ATR(14) from the fill, then a chandelier trail 3 x ATR |
|
| //| behind the best price since entry, tightened once per H4 bar. |
|
| //| No target. An opposite signal reverses; a same-side signal |
|
| //| while in a trade is ignored. ATRs are Wilder's (RMA) seeded |
|
| //| with an SMA, as TradingView computes them. |
|
| //| Candles: the rule was researched on New York-close candles |
|
| //| (server time UTC+2, or UTC+3 while US daylight saving is on). |
|
| //| On a broker with another server clock, H4 and D1 candles are |
|
| //| rebuilt from H1 on that clock, so the signals stay the same. |
|
| //| Lot: always 0.01, or the broker's minimum when that is larger; |
|
| //| no lot inputs. No network calls. |
|
| //| Every order is checked for margin and for surviving its stop |
|
| //| above the broker's stop-out level. Live and demo charts trade |
|
| //| XAUUSD only; the Strategy Tester may test any symbol. |
|
| //+------------------------------------------------------------------+
|
| // SPDX-License-Identifier: MPL-2.0
|
| // This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not
|
| // distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/
|
| // Origin of the maths: UT Bot idea by Yo_adriiiiaan (from an original idea by HPotter), TradingView "UT Bot Alerts" by
|
| // QuantNomad; Supertrend is the textbook ATR-band indicator.
|
|
|
| //+------------------------------------------------------------------+
|
| //| Inputs |
|
| //+------------------------------------------------------------------+
|
| //--- Inputs-dialog text. MT5 shows each enum choice by the comment on its own line, so every member,
|
| //--- including the last, needs its comment before the closing brace.
|
| enum ENUM_GTS_CORNER
|
| {
|
| GTS_LEFT=0, // Top left
|
| GTS_RIGHT=1 // Top right
|
| };
|
|
|
| enum ENUM_GTS_CLOCK
|
| {
|
| GTS_CLOCK_AUTO=0, // Auto: detect from the live server time
|
| GTS_CLOCK_NY=1, // New York close: UTC+2/+3 with US daylight saving
|
| GTS_CLOCK_EET=2, // UTC+2/+3 with European daylight saving
|
| GTS_CLOCK_FIXED=3 // Fixed offset from UTC (set below), no daylight saving
|
| };
|
|
|
| input int InpMagic = 770100; // Magic number (order ID)
|
| input ENUM_TIMEFRAMES InpTF = PERIOD_H4; // Signal timeframe
|
| input double InpUTKey = 1.5; // UT Bot key value
|
| input int InpUTATR = 10; // UT Bot ATR period
|
| input int InpSTPeriod = 10; // Supertrend ATR period
|
| input double InpSTMult = 3.0; // Supertrend multiplier
|
| input int InpTrendEMA = 200; // Daily trend EMA period
|
| input int InpStopATR = 14; // Stop ATR period
|
| input double InpStopMult = 3.0; // Stop and trail, x ATR
|
| input bool InpTrail = true; // Trail the stop
|
|
|
| input ENUM_GTS_CLOCK InpClock = GTS_CLOCK_AUTO; // Broker server clock
|
| input int InpFixedUTC = 0; // Broker offset from UTC, hours (Fixed offset only)
|
|
|
| input bool InpShowPanel = true; // Show info panel
|
| input bool InpShowLevels = true; // Show the UT Bot line and signals
|
| input bool InpLightTheme = false; // Light colours (for white charts)
|
| input ENUM_GTS_CORNER InpCorner = GTS_LEFT; // Panel position
|
| input int InpOffsetX = 10; // Panel margin from the side, px
|
| input int InpOffsetY = 26; // Panel margin from the top, px
|
| input int InpFontSize = 9; // Panel font size
|
|
|
| input bool InpDebug = false; // Debug log for bug reports (Experts tab)
|
|
|
| //+------------------------------------------------------------------+
|
| //| Constants and state |
|
| //+------------------------------------------------------------------+
|
| #define WIN 1500 // signal-timeframe bars the indicators are rebuilt over
|
| #define D1WIN 1500 // daily bars for the EMA(200)
|
| #define MIN_BARS 300 // signal bars required before trading
|
| #define PREFIX "GTS_" // chart object prefix
|
| #define MARKERS 60 // signal markers kept on the chart
|
| #define GAP_MULT 2.0 // plan for twice the loss at the stop: a gap, or a tester that only
|
| // checks stops at bar opens, can carry the price past it
|
|
|
| datetime g_last_bar = 0; // open time of the last processed signal bar
|
|
|
| //--- symbol properties cached in OnInit
|
| double g_tick=0, g_vol_min=0, g_vol_max=0, g_vol_step=0;
|
|
|
| //--- status
|
| bool g_symbol_ok=true; // XAUUSD on live charts (any symbol in the tester)
|
|
|
| //--- what the last evaluation saw, for the panel and the trail
|
| double g_ut_stop=0; // UT Bot line on the last closed bar
|
| int g_ut_side=0; // +1 close above the line, -1 below
|
| int g_st_dir=0; // Supertrend +1 up / -1 down
|
| int g_d1_dir=0; // daily close vs EMA: +1 above / -1 below
|
| double g_d1_close=0, g_d1_ema=0;
|
| double g_atr_stop=0; // ATR(14) on the last closed bar
|
| double g_pdh=0; // reference price for the Risk row (current bid)
|
| double g_ref_stop=0; // stop distance the next trade would use
|
| datetime g_sig_time=0; // bar of the last buy/sell signal
|
| int g_sig_side=0; // +1 buy, -1 sell
|
| string g_sig_result=""; // "placed" / "reversed" / why it was skipped
|
| bool g_market_open=true;
|
| //--- candle clock
|
| bool g_native=true; // the broker already runs on New York-close time: use its own candles
|
| ENUM_GTS_CLOCK g_clock=GTS_CLOCK_NY; // resolved clock (Auto becomes one of the others)
|
| int g_fixed_off=0; // seconds, for GTS_CLOCK_FIXED
|
|
|
| //--- panel state
|
| bool g_ui=false, g_collapsed=false, g_stats_dirty=true;
|
| int g_rows=0;
|
| string g_cache_txt[];
|
| int g_cache_x[], g_cache_y[], g_cache_w[], g_cache_h[]; // last geometry sent per object
|
| color g_cache_fill[];
|
| int g_cursor=0; // the panel draws in the same order every time: try this slot first
|
| bool g_panel_dirty=true; // something changed since the last ChartRedraw
|
| datetime g_cache_reset=0; // the cache is dropped once a minute, so deleted objects come back
|
| color g_cache_clr[];
|
| string g_cache_name[];
|
| int g_closed=0, g_wins=0;
|
| int g_hist_seen=0; // closed-trade results are read incrementally: history records counted
|
| datetime g_today_start=0;
|
| int g_trd[]; // this EA's open orders, watched until they close (MQL4: no OnTradeTransaction)
|
| double g_trd_sl[]; // their stop at entry (debug TRD lines)
|
| double g_net=0, g_gross_win=0, g_gross_loss=0, g_today=0;
|
| double g_curve=0, g_curve_peak=0, g_max_dd=0; // closed-trade results in sequence, for the worst drawdown
|
| int g_mon_key[]; // yyyymm of the months with closed trades
|
| double g_mon_net[]; // their results
|
| int g_markers=0;
|
| datetime g_last_ui=0;
|
|
|
| //+------------------------------------------------------------------+
|
| //| Palette (same as Vitals) |
|
| //+------------------------------------------------------------------+
|
| color UiBack() { return(InpLightTheme ? C'250,250,251' : C'24,26,30'); } // panel background
|
| color UiText() { return(InpLightTheme ? C'44,48,56' : C'206,212,220'); } // values
|
| color UiLabel() { return(InpLightTheme ? C'124,131,142' : C'122,130,142'); } // field names
|
| color UiHead() { return(InpLightTheme ? C'92,100,112' : C'150,160,175'); } // section titles
|
| color UiDim() { return(InpLightTheme ? C'140,146,157' : C'96,102,112'); } // zero / no data
|
| color UiPos() { return(InpLightTheme ? C'38,118,66' : C'126,192,143'); } // sage, not Lime
|
| color UiNeg() { return(InpLightTheme ? C'176,58,58' : C'226,124,124'); } // coral, not Red
|
| color UiWarn() { return(InpLightTheme ? C'160,112,28' : C'220,178,116'); } // amber
|
| color UiAccent() { return(InpLightTheme ? C'48,92,148' : C'132,164,206'); } // market data
|
| color UiRule() { return(InpLightTheme ? C'220,223,228' : C'48,52,58'); } // group divider
|
|
|
| //+------------------------------------------------------------------+
|
| //| Panel geometry, px at font size 9 |
|
| //+------------------------------------------------------------------+
|
| #define UI_W 306 // panel width (27 characters of value)
|
| #define UI_PAD 10 // inner padding
|
| #define UI_VAL 106 // value column (clears "Profit factor" at font 9)
|
| #define UI_ROW 16 // row height
|
| #define UI_HEAD 24 // title bar height
|
| #define UI_GAP 6 // space above a section title
|
| #define UI_CHAR 7 // Consolas 9 advance width, px
|
|
|
| //+------------------------------------------------------------------+
|
| //| Rounds a price to the symbol tick size |
|
| //+------------------------------------------------------------------+
|
| double FixPrice(const double price)
|
| {
|
| if(g_tick==0)
|
| return(NormalizeDouble(price,_Digits));
|
| return(NormalizeDouble(MathRound(price/g_tick)*g_tick,_Digits));
|
| }
|
|
|
| //--- server time: TimeCurrent() in the tester; live, MQL4 has no TimeTradeServer and TimeCurrent stops between ticks
|
| //--- (all weekend), so GMT + the server's offset, learnt from a fresh tick (a whole half hour, -12 h .. +14 h)
|
| datetime NowSrv(void)
|
| {
|
| static long off=LONG_MIN;
|
| if(IsTesting())
|
| return(TimeCurrent());
|
| long d=(long)TimeCurrent()-(long)TimeGMT();
|
| long r=(long)MathRound(d/1800.0)*1800;
|
| if(MathAbs(d-r)<=120 && r>=-12*3600 && r<=14*3600)
|
| off=r;
|
| return(off==LONG_MIN ? TimeCurrent() : (datetime)((long)TimeGMT()+off));
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| True inside a trading session of the chart symbol, server time |
|
| //+------------------------------------------------------------------+
|
| bool IsMarketOpen(void)
|
| {
|
| datetime now=NowSrv();
|
| MqlDateTime dt;
|
| TimeToStruct(now,dt);
|
| int tod=(int)(now%86400);
|
| datetime from,to;
|
| for(uint s=0; SymbolInfoSessionTrade(_Symbol,(ENUM_DAY_OF_WEEK)dt.day_of_week,s,from,to); s++)
|
| {
|
| int f=(int)(from%86400),t=(int)(to%86400);
|
| if(f<t ? (tod>=f && tod<t) : (tod>=f || tod<t)) // the session end itself is closed (orders rejected)
|
| return(true);
|
| }
|
| return(false);
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Selects order `i` of the open pool; true for a market order of |
|
| //| this EA on this symbol |
|
| //+------------------------------------------------------------------+
|
| bool MyOrderAt(const int i)
|
| {
|
| return(OrderSelect(i,SELECT_BY_POS,MODE_TRADES) && OrderType()<=OP_SELL && OrderMagicNumber()==InpMagic &&
|
| OrderSymbol()==_Symbol);
|
| }
|
|
|
| //--- true when ticket `t` is still open (and selects it)
|
| bool OpenTicket(const int t) { return(t>0 && OrderSelect(t,SELECT_BY_TICKET) && OrderCloseTime()==0); }
|
|
|
| double Bid_(void) { return(MarketInfo(_Symbol,MODE_BID)); }
|
| double Ask_(void) { return(MarketInfo(_Symbol,MODE_ASK)); }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Risk shown on the panel: the percent of balance lost at the stop,|
|
| //| and its name. Bands: Safe <= 1, Medium <= 2, High <= 3, Very high|
|
| //| <= 5, Extreme above. |
|
| //+------------------------------------------------------------------+
|
|
|
| string RiskName(const double pct)
|
| {
|
| if(pct<=1.0) return("Safe");
|
| if(pct<=2.0) return("Medium");
|
| if(pct<=3.0) return("High");
|
| if(pct<=5.0) return("Very high");
|
| return("Extreme");
|
| }
|
|
|
| color RiskColor(const double pct)
|
| {
|
| if(pct<=1.0) return(UiPos());
|
| if(pct<=2.0) return(UiAccent());
|
| if(pct<=3.0) return(UiWarn());
|
| return(UiNeg());
|
| }
|
|
|
| //--- the money one tick moves one lot, in the account currency. MT4 gives 0 for gold in the Strategy Tester: then tick
|
| //--- size x contract size, in the profit currency, converted with its cross rate
|
| double TickValue(void)
|
| {
|
| double tv=MarketInfo(_Symbol,MODE_TICKVALUE);
|
| if(tv>0)
|
| return(tv);
|
| double v=MarketInfo(_Symbol,MODE_TICKSIZE)*MarketInfo(_Symbol,MODE_LOTSIZE);
|
| string pc=SymbolInfoString(_Symbol,SYMBOL_CURRENCY_PROFIT),ac=AccountCurrency();
|
| if(v<=0 || pc=="")
|
| return(0);
|
| if(pc==ac)
|
| return(v);
|
| double r=MarketInfo(pc+ac,MODE_BID);
|
| if(r>0)
|
| return(v*r);
|
| r=MarketInfo(ac+pc,MODE_BID);
|
| return(r>0 ? v/r : 0);
|
| }
|
|
|
| //--- money lost at the stop by one lot (MQL4 has no OrderCalcProfit: ticks x tick value)
|
| double LossPerLot(const double price,const double sl)
|
| {
|
| return(g_tick>0 ? MathAbs(price-sl)/g_tick*TickValue() : 0);
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| The minimum lot: 0.01, or the broker's minimum on |
|
| //| this symbol when that is larger (0.10 at some brokers). A broker |
|
| //| that allows less than 0.01 still gets 0.01. |
|
| //+------------------------------------------------------------------+
|
| double MinLot(void)
|
| {
|
| double v=MathMax(0.01,g_vol_min);
|
| if(g_vol_step>0) // keep it on the broker's lot step
|
| v=MathCeil(v/g_vol_step-1e-9)*g_vol_step;
|
| return(NormalizeDouble(v,8));
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //+------------------------------------------------------------------+
|
| //| Lot for an order at `price` with stop `sl`: always the broker's |
|
| //| minimum lot (SYMBOL_VOLUME_MIN: 0.01 on most brokers, 0.10 on |
|
| //| some), whatever the balance. |
|
| //+------------------------------------------------------------------+
|
| double TradeVolume(const double price,const double sl,string &why)
|
| {
|
| return(MinLot()); // FREE: 0.01 or the broker minimum, whatever the balance
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| "" when the account can take a new position of `volume` at |
|
| //| `price` with stop `sl`; otherwise the reason. Two conditions: |
|
| //| the margin fits in free margin, and if every open position of |
|
| //| this EA and the new one all reach their stops, equity stays above|
|
| //| the broker's stop-out level for the margin then in use. A broker |
|
| //| would reject the first and force-close the second. |
|
| //+------------------------------------------------------------------+
|
| string Affordable(const bool buy,const double volume,const double price,const double sl)
|
| {
|
| //--- 0 when MT4 cannot price the margin (the tester offline, "margin exchange rate cannot be calculated"): the
|
| //--- broker decides
|
| double margin=volume*MarketInfo(_Symbol,MODE_MARGINREQUIRED);
|
| if(margin>AccountInfoDouble(ACCOUNT_MARGIN_FREE))
|
| return("not enough margin");
|
| double worst=AccountInfoDouble(ACCOUNT_EQUITY)-GAP_MULT*volume*LossPerLot(price,sl);
|
| for(int i=OrdersTotal()-1; i>=0; i--)
|
| {
|
| if(!MyOrderAt(i))
|
| continue;
|
| double psl=OrderStopLoss();
|
| bool pbuy=OrderType()==OP_BUY;
|
| double cur=pbuy ? Bid_() : Ask_();
|
| if(psl>0 && (pbuy ? psl<cur : psl>cur)) // room left between the price now and its stop
|
| worst-=GAP_MULT*OrderLots()*LossPerLot(cur,psl);
|
| }
|
| double so=AccountInfoDouble(ACCOUNT_MARGIN_SO_SO);
|
| double used=AccountInfoDouble(ACCOUNT_MARGIN)+margin;
|
| double floor_=AccountInfoInteger(ACCOUNT_MARGIN_SO_MODE)==ACCOUNT_STOPOUT_MODE_PERCENT ? used*so/100.0 : so;
|
| if(worst<=floor_)
|
| return("balance too small for SL");
|
| return("");
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Chart objects: one cached setter so unchanged text is not resent |
|
| //+------------------------------------------------------------------+
|
| int CacheSlot(const string name)
|
| {
|
| int n=ArraySize(g_cache_name);
|
| if(g_cursor<n && g_cache_name[g_cursor]==name)
|
| return(g_cursor++);
|
| for(int i=0; i<n; i++)
|
| if(g_cache_name[i]==name)
|
| {
|
| g_cursor=i+1;
|
| return(i);
|
| }
|
| ArrayResize(g_cache_name,n+1);
|
| ArrayResize(g_cache_txt,n+1);
|
| ArrayResize(g_cache_clr,n+1);
|
| ArrayResize(g_cache_x,n+1);
|
| ArrayResize(g_cache_y,n+1);
|
| ArrayResize(g_cache_w,n+1);
|
| ArrayResize(g_cache_h,n+1);
|
| ArrayResize(g_cache_fill,n+1);
|
| g_cache_name[n]=name;
|
| g_cache_txt[n]="\x01";
|
| g_cache_clr[n]=clrNONE;
|
| g_cache_fill[n]=clrNONE;
|
| g_cache_x[n]=g_cache_y[n]=g_cache_w[n]=g_cache_h[n]=INT_MIN;
|
| g_cursor=n+1;
|
| return(n);
|
| }
|
|
|
| void CacheClear(void)
|
| {
|
| ArrayResize(g_cache_name,0);
|
| ArrayResize(g_cache_txt,0);
|
| ArrayResize(g_cache_clr,0);
|
| ArrayResize(g_cache_x,0);
|
| ArrayResize(g_cache_y,0);
|
| ArrayResize(g_cache_w,0);
|
| ArrayResize(g_cache_h,0);
|
| ArrayResize(g_cache_fill,0);
|
| g_cursor=0;
|
| g_panel_dirty=true;
|
| }
|
|
|
| //--- sets an integer property only when it differs from what was last sent
|
| void SetInt(const string name,const ENUM_OBJECT_PROPERTY_INTEGER prop,const long v,int &cache)
|
| {
|
| if(cache==(int)v)
|
| return;
|
| ObjectSetInteger(0,name,prop,v);
|
| cache=(int)v;
|
| g_panel_dirty=true;
|
| }
|
|
|
| //--- x distance of a point `x` px from the panel's left edge, for either corner
|
| int PanelX(const int x)
|
| {
|
| return(InpCorner==GTS_LEFT ? InpOffsetX+x : InpOffsetX+UI_W-x);
|
| }
|
|
|
| ENUM_BASE_CORNER PanelCorner(void)
|
| {
|
| return(InpCorner==GTS_LEFT ? CORNER_LEFT_UPPER : CORNER_RIGHT_UPPER);
|
| }
|
|
|
| void Text(const string id,const int x,const int y,const string text,const color clr,
|
| const int size=0,const string font="Consolas")
|
| {
|
| string name=PREFIX+id;
|
| string txt=(text=="") ? " " : text; // an empty label would show the word "Label"
|
| int slot=CacheSlot(name);
|
| if(g_cache_x[slot]==INT_MIN && ObjectFind(0,name)<0) // first time this object is seen
|
| {
|
| ObjectCreate(0,name,OBJ_LABEL,0,0,0);
|
| ObjectSetInteger(0,name,OBJPROP_CORNER,PanelCorner());
|
| ObjectSetInteger(0,name,OBJPROP_ANCHOR,ANCHOR_LEFT_UPPER);
|
| ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
|
| ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
|
| ObjectSetInteger(0,name,OBJPROP_BACK,false);
|
| ObjectSetString(0,name,OBJPROP_FONT,font);
|
| ObjectSetInteger(0,name,OBJPROP_FONTSIZE,size>0 ? size : InpFontSize);
|
| }
|
| SetInt(name,OBJPROP_XDISTANCE,PanelX(x),g_cache_x[slot]);
|
| SetInt(name,OBJPROP_YDISTANCE,InpOffsetY+y,g_cache_y[slot]);
|
| if(g_cache_txt[slot]!=txt)
|
| {
|
| ObjectSetString(0,name,OBJPROP_TEXT,txt);
|
| g_cache_txt[slot]=txt;
|
| g_panel_dirty=true;
|
| }
|
| if(g_cache_clr[slot]!=clr)
|
| {
|
| ObjectSetInteger(0,name,OBJPROP_COLOR,clr);
|
| g_cache_clr[slot]=clr;
|
| g_panel_dirty=true;
|
| }
|
| }
|
|
|
| void Box(const string id,const int x,const int y,const int w,const int h,const color fill,const color border)
|
| {
|
| string name=PREFIX+id;
|
| int slot=CacheSlot(name);
|
| if(g_cache_x[slot]==INT_MIN && ObjectFind(0,name)<0)
|
| {
|
| ObjectCreate(0,name,OBJ_RECTANGLE_LABEL,0,0,0);
|
| ObjectSetInteger(0,name,OBJPROP_CORNER,PanelCorner());
|
| ObjectSetInteger(0,name,OBJPROP_BORDER_TYPE,BORDER_FLAT);
|
| ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
|
| ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
|
| ObjectSetInteger(0,name,OBJPROP_BACK,false);
|
| }
|
| //--- a rectangle label is anchored at its top-left corner, so on the right corner shift it by its width
|
| SetInt(name,OBJPROP_XDISTANCE,InpCorner==GTS_LEFT ? InpOffsetX+x : InpOffsetX+UI_W-x,g_cache_x[slot]);
|
| SetInt(name,OBJPROP_YDISTANCE,InpOffsetY+y,g_cache_y[slot]);
|
| SetInt(name,OBJPROP_XSIZE,w,g_cache_w[slot]);
|
| SetInt(name,OBJPROP_YSIZE,h,g_cache_h[slot]);
|
| if(g_cache_fill[slot]!=fill)
|
| {
|
| ObjectSetInteger(0,name,OBJPROP_BGCOLOR,fill);
|
| g_cache_fill[slot]=fill;
|
| g_panel_dirty=true;
|
| }
|
| if(g_cache_clr[slot]!=border)
|
| {
|
| ObjectSetInteger(0,name,OBJPROP_COLOR,border);
|
| g_cache_clr[slot]=border;
|
| g_panel_dirty=true;
|
| }
|
| }
|
|
|
| void Button(const string id,const int x,const int y,const string txt)
|
| {
|
| string name=PREFIX+id;
|
| if(ObjectFind(0,name)<0)
|
| {
|
| ObjectCreate(0,name,OBJ_BUTTON,0,0,0);
|
| ObjectSetInteger(0,name,OBJPROP_CORNER,PanelCorner());
|
| ObjectSetInteger(0,name,OBJPROP_XSIZE,18);
|
| ObjectSetInteger(0,name,OBJPROP_YSIZE,16);
|
| ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
|
| ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
|
| ObjectSetString(0,name,OBJPROP_FONT,"Consolas");
|
| ObjectSetInteger(0,name,OBJPROP_FONTSIZE,InpFontSize);
|
| }
|
| ObjectSetInteger(0,name,OBJPROP_XDISTANCE,InpCorner==GTS_LEFT ? InpOffsetX+x : InpOffsetX+UI_W-x);
|
| ObjectSetInteger(0,name,OBJPROP_YDISTANCE,InpOffsetY+y);
|
| ObjectSetInteger(0,name,OBJPROP_BGCOLOR,UiBack());
|
| ObjectSetInteger(0,name,OBJPROP_BORDER_COLOR,UiRule());
|
| ObjectSetInteger(0,name,OBJPROP_COLOR,UiHead());
|
| ObjectSetString(0,name,OBJPROP_TEXT,txt);
|
| ObjectSetInteger(0,name,OBJPROP_STATE,false);
|
| }
|
|
|
| //--- one "label value" row; returns the next y
|
| int Row(const string id,const int y,const string label,const string value,const color clr)
|
| {
|
| int fit=(UI_W-UI_VAL-UI_PAD)/UI_CHAR; // characters that fit in the value column
|
| Text(id+"_l",UI_PAD,y,label,UiLabel());
|
| Text(id+"_v",UI_VAL,y,StringLen(value)>fit ? StringSubstr(value,0,fit-1)+"~" : value,clr);
|
| return(y+UI_ROW);
|
| }
|
|
|
| int Section(const string id,const int y,const string title)
|
| {
|
| int top=y+UI_GAP;
|
| Box(id+"_r",UI_PAD,top+UI_ROW-3,UI_W-2*UI_PAD,1,UiRule(),UiRule());
|
| Text(id+"_t",UI_PAD,top,title,UiHead(),InpFontSize-1);
|
| return(top+UI_ROW+2);
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Formatting helpers |
|
| //+------------------------------------------------------------------+
|
| string Px(const double v) { return(v>0 ? DoubleToString(v,_Digits) : "-"); } // 2 or 3 digit gold alike
|
| string Money(const double v) { return((v>=0 ? "+" : "-")+DoubleToString(MathAbs(v),2)+" "+AccountInfoString(ACCOUNT_CURRENCY)); }
|
| color Sign(const double v) { return(v>0 ? UiPos() : (v<0 ? UiNeg() : UiDim())); }
|
|
|
| //--- the lot the EA uses: 0.01, or the broker minimum
|
| string LotText(void)
|
| {
|
| return(DoubleToString(MinLot(),2)+" fixed (FREE)");
|
| }
|
|
|
| //--- percent of balance lost at the stop with the lot actually used, at yesterday's high
|
| double g_risk_now=0;
|
| string RiskText(void)
|
| {
|
| g_risk_now=0;
|
| double bal=AccountInfoDouble(ACCOUNT_BALANCE);
|
| if(g_pdh<=0 || g_ref_stop<=0 || bal<=0)
|
| return("-");
|
| string why="";
|
| double sl=g_pdh-g_ref_stop;
|
| double v=TradeVolume(g_pdh,sl,why);
|
| if(v<=0)
|
| return("-");
|
| g_risk_now=100.0*v*LossPerLot(g_pdh,sl)/bal;
|
| return(StringFormat("%.1f%% at stop %s",g_risk_now,RiskName(g_risk_now)));
|
| }
|
|
|
| string Clock(const int secs)
|
| {
|
| int s=MathMax(0,secs);
|
| return(s>=3600 ? StringFormat("%dh %02dm",s/3600,(s%3600)/60) : StringFormat("%02d:%02d",s/60,s%60));
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Closed-trade results of this EA on this symbol, from the account |
|
| //| history (MT4: one record per closed order or closed part). |
|
| //| Read incrementally: only records added since the last update. |
|
| //+------------------------------------------------------------------+
|
| void RecalcStats(void)
|
| {
|
| datetime today=iTime(_Symbol,PERIOD_D1,0);
|
| if(today!=g_today_start) // a new day: today's total starts again
|
| {
|
| g_today_start=today;
|
| g_today=0;
|
| for(int i=0; i<OrdersHistoryTotal(); i++) // today's closed trades so far
|
| if(OrderSelect(i,SELECT_BY_POS,MODE_HISTORY) && OrderType()<=OP_SELL && OrderMagicNumber()==InpMagic &&
|
| OrderSymbol()==_Symbol && OrderCloseTime()>=today && i<g_hist_seen)
|
| g_today+=OrderProfit()+OrderSwap()+OrderCommission();
|
| }
|
| int n=OrdersHistoryTotal();
|
| if(n<g_hist_seen) // the history tab's period was shortened: count again
|
| {
|
| g_hist_seen=0;
|
| g_closed=g_wins=0;
|
| g_net=g_gross_win=g_gross_loss=g_today=g_curve=g_curve_peak=g_max_dd=0;
|
| ArrayResize(g_mon_key,0);
|
| ArrayResize(g_mon_net,0);
|
| }
|
| for(int i=g_hist_seen; i<n; i++)
|
| {
|
| if(!OrderSelect(i,SELECT_BY_POS,MODE_HISTORY) || OrderType()>OP_SELL || OrderMagicNumber()!=InpMagic ||
|
| OrderSymbol()!=_Symbol)
|
| continue;
|
| double v=OrderProfit()+OrderSwap()+OrderCommission();
|
| datetime t=OrderCloseTime();
|
| g_net+=v;
|
| if(t>=today)
|
| g_today+=v;
|
| g_closed++;
|
| g_curve+=v;
|
| g_curve_peak=MathMax(g_curve_peak,g_curve);
|
| g_max_dd=MathMax(g_max_dd,g_curve_peak-g_curve);
|
| MqlDateTime mt;
|
| TimeToStruct(t,mt);
|
| int key=mt.year*100+mt.mon,k=ArraySize(g_mon_key)-1;
|
| if(k<0 || g_mon_key[k]!=key)
|
| {
|
| k++;
|
| ArrayResize(g_mon_key,k+1);
|
| ArrayResize(g_mon_net,k+1);
|
| g_mon_key[k]=key;
|
| g_mon_net[k]=0;
|
| }
|
| g_mon_net[k]+=v;
|
| if(v>0)
|
| {
|
| g_wins++;
|
| g_gross_win+=v;
|
| }
|
| else
|
| g_gross_loss-=v;
|
| }
|
| g_hist_seen=n;
|
| g_stats_dirty=false;
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Indicators, exactly as the Python research computes them |
|
| //+------------------------------------------------------------------+
|
| //--- Sunday on or after `day` of `month` in year `y`, at `hour` UTC
|
| datetime SundayFrom(const int y,const int month,const int day,const int hour)
|
| {
|
| MqlDateTime t;
|
| ZeroMemory(t);
|
| t.year=y;
|
| t.mon=month;
|
| t.day=day;
|
| datetime d=StructToTime(t);
|
| TimeToStruct(d,t);
|
| return(d+(datetime)(((7-t.day_of_week)%7)*86400+hour*3600));
|
| }
|
|
|
| //--- US daylight saving (second Sunday of March 07:00 UTC to first Sunday of November 06:00 UTC)
|
| bool UsDst(const datetime utc)
|
| {
|
| MqlDateTime t;
|
| TimeToStruct(utc,t);
|
| return(utc>=SundayFrom(t.year,3,8,7) && utc<SundayFrom(t.year,11,1,6));
|
| }
|
|
|
| //--- EU summer time (last Sunday of March to last Sunday of October, 01:00 UTC)
|
| bool EuDst(const datetime utc)
|
| {
|
| MqlDateTime t;
|
| TimeToStruct(utc,t);
|
| return(utc>=SundayFrom(t.year,3,25,1) && utc<SundayFrom(t.year,10,25,1));
|
| }
|
|
|
| int NyOffset(const datetime utc) { return(UsDst(utc) ? 3*3600 : 2*3600); }
|
|
|
| //--- the broker's offset from UTC at server time `srv`
|
| int BrokerOffset(const datetime srv)
|
| {
|
| if(g_clock==GTS_CLOCK_FIXED)
|
| return(g_fixed_off);
|
| if(g_clock==GTS_CLOCK_EET)
|
| return(EuDst(srv-2*3600) ? 3*3600 : 2*3600);
|
| return(NyOffset(srv-2*3600));
|
| }
|
|
|
| //--- seconds to add to server time to get New York-close time
|
| int Shift(const datetime srv)
|
| {
|
| if(g_native)
|
| return(0);
|
| int b=BrokerOffset(srv);
|
| return(NyOffset(srv-b)-b);
|
| }
|
|
|
| //--- open time, on the New York-close clock, of the candle of length `per` that contains server time `srv`
|
| datetime NyBucket(const datetime srv,const int per)
|
| {
|
| long ny=(long)srv+Shift(srv);
|
| return((datetime)(ny-ny%per));
|
| }
|
|
|
| //--- decide the clock once: the inputs, or the live server time against UTC (the tester can't tell, see OnInit)
|
| void ResolveClock(void)
|
| {
|
| g_clock=InpClock;
|
| g_fixed_off=InpFixedUTC*3600;
|
| if(InpClock==GTS_CLOCK_AUTO)
|
| {
|
| g_clock=GTS_CLOCK_NY;
|
| if(!MQLInfoInteger(MQL_TESTER))
|
| {
|
| datetime gmt=TimeGMT();
|
| int off=(int)MathRound((double)(NowSrv()-gmt)/1800.0)*1800;
|
| if(off==NyOffset(gmt))
|
| g_clock=GTS_CLOCK_NY;
|
| else
|
| if(off==(EuDst(gmt) ? 3*3600 : 2*3600))
|
| g_clock=GTS_CLOCK_EET;
|
| else
|
| {
|
| g_clock=GTS_CLOCK_FIXED;
|
| g_fixed_off=off;
|
| }
|
| }
|
| }
|
| g_native=(g_clock==GTS_CLOCK_NY);
|
| }
|
|
|
| string ClockText(void)
|
| {
|
| if(g_native)
|
| return("NY close (broker)");
|
| if(g_clock==GTS_CLOCK_EET)
|
| return("NY close, from EET");
|
| return(StringFormat("NY close, from UTC%+d",g_fixed_off/3600));
|
| }
|
|
|
| //--- closed candles of `tf`, oldest first, on the New York-close clock. With a New York-close broker this is the
|
| //--- broker's own candles; otherwise they are rebuilt from H1 (times in the result are New York-close times)
|
| int GetBars(const ENUM_TIMEFRAMES tf,const int count,MqlRates &out[])
|
| {
|
| int per=PeriodSeconds(tf);
|
| if(g_native || per<3600 || per>86400 || per%3600!=0)
|
| return(CopyRates(_Symbol,tf,1,count,out));
|
| MqlRates h[];
|
| int need=(int)MathMin((double)count*per/3600.0+48.0,60000.0);
|
| int n=CopyRates(_Symbol,PERIOD_H1,1,need,h);
|
| if(n<=0)
|
| return(n);
|
| datetime cur=NyBucket(TimeCurrent(),per); // the candle still forming is left out
|
| ArrayResize(out,0,count+1);
|
| int m=0;
|
| for(int i=0; i<n; i++)
|
| {
|
| datetime k=NyBucket(h[i].time,per);
|
| if(k>=cur)
|
| break;
|
| if(m==0 || k!=out[m-1].time)
|
| {
|
| ArrayResize(out,m+1,count+1);
|
| out[m]=h[i];
|
| out[m].time=k;
|
| m++;
|
| continue;
|
| }
|
| out[m-1].high=MathMax(out[m-1].high,h[i].high);
|
| if(h[i].low<out[m-1].low)
|
| {
|
| out[m-1].low=h[i].low;
|
| out[m-1].spread=h[i].spread; // the spread when the low printed: the ask low for shorts
|
| }
|
| out[m-1].close=h[i].close;
|
| out[m-1].tick_volume+=h[i].tick_volume;
|
| out[m-1].real_volume+=h[i].real_volume;
|
| }
|
| if(m>count) // keep the newest `count` (MQL4 has no ArrayRemove)
|
| {
|
| MqlRates keep[];
|
| ArrayCopy(keep,out,0,m-count,count);
|
| ArrayFree(out);
|
| ArrayCopy(out,keep);
|
| m=count;
|
| }
|
| return(m);
|
| }
|
|
|
| //--- open time of the forming candle (New York-close time when converting), and seconds until it closes
|
| datetime CurBar(void) { return(g_native ? iTime(_Symbol,InpTF,0) : NyBucket(TimeCurrent(),PeriodSeconds(InpTF))); }
|
|
|
| int SecsToNextBar(void)
|
| {
|
| datetime now=TimeCurrent();
|
| return(PeriodSeconds(InpTF)-(int)(now+Shift(now)-CurBar()));
|
| }
|
|
|
| //--- true range; the first bar has no previous close, so it is high - low
|
| void TrueRange(const MqlRates &r[],const int n,double &tr[])
|
| {
|
| ArrayResize(tr,n);
|
| for(int i=0; i<n; i++)
|
| {
|
| double hl=r[i].high-r[i].low;
|
| if(i==0)
|
| {
|
| tr[i]=hl;
|
| continue;
|
| }
|
| double pc=r[i-1].close;
|
| tr[i]=MathMax(hl,MathMax(MathAbs(r[i].high-pc),MathAbs(r[i].low-pc)));
|
| }
|
| }
|
|
|
| //--- Wilder's smoothing (TradingView ta.rma), seeded with the SMA of the first n values; EMPTY_VALUE before
|
| void Rma(const double &x[],const int n,const int period,double &out[])
|
| {
|
| ArrayResize(out,n);
|
| ArrayInitialize(out,EMPTY_VALUE);
|
| if(n<period)
|
| return;
|
| double s=0;
|
| for(int i=0; i<period; i++)
|
| s+=x[i];
|
| out[period-1]=s/period;
|
| for(int i=period; i<n; i++)
|
| out[i]=(out[i-1]*(period-1)+x[i])/period;
|
| }
|
|
|
| //--- UT Bot Alerts trailing line on the close
|
| void UTBot(const MqlRates &r[],const int n,const double &atr[],const double key,double &stop[])
|
| {
|
| ArrayResize(stop,n);
|
| for(int i=0; i<n; i++)
|
| {
|
| double prev=(i>0 ? stop[i-1] : 0.0);
|
| if(atr[i]==EMPTY_VALUE)
|
| {
|
| stop[i]=(i==0 ? EMPTY_VALUE : prev);
|
| continue;
|
| }
|
| if(prev==EMPTY_VALUE)
|
| prev=0.0;
|
| double src=r[i].close,p1=(i>0 ? r[i-1].close : r[i].close),nloss=key*atr[i];
|
| if(src>prev && p1>prev)
|
| stop[i]=MathMax(prev,src-nloss);
|
| else
|
| if(src<prev && p1<prev)
|
| stop[i]=MathMin(prev,src+nloss);
|
| else
|
| stop[i]=(src>prev ? src-nloss : src+nloss);
|
| }
|
| }
|
|
|
| //--- Supertrend direction (+1 up, -1 down; 0 before the ATR exists)
|
| void Supertrend(const MqlRates &r[],const int n,const double &atr[],const double mult,int &dir[])
|
| {
|
| ArrayResize(dir,n);
|
| ArrayInitialize(dir,0);
|
| double up=0,dn=0,st=0;
|
| bool have=false;
|
| for(int i=0; i<n; i++)
|
| {
|
| if(atr[i]==EMPTY_VALUE)
|
| continue;
|
| double src=(r[i].high+r[i].low)/2.0;
|
| double ub=src+mult*atr[i],lb=src-mult*atr[i];
|
| if(have)
|
| {
|
| lb=(lb>dn || r[i-1].close<dn) ? lb : dn;
|
| ub=(ub<up || r[i-1].close>up) ? ub : up;
|
| }
|
| double d;
|
| if(!have)
|
| d=1.0;
|
| else
|
| if(st==up)
|
| d=(r[i].close>ub ? -1.0 : 1.0);
|
| else
|
| d=(r[i].close<lb ? 1.0 : -1.0);
|
| up=ub;
|
| dn=lb;
|
| st=(d<0 ? lb : ub);
|
| have=true;
|
| dir[i]=(d<0 ? 1 : -1); // research: trend = -direction
|
| }
|
| }
|
|
|
| //--- daily trend: last completed D1 close vs its EMA (seeded with the SMA of the first `period` closes)
|
| bool DailyTrend(int &dir,double &close1,double &ema1)
|
| {
|
| double c[];
|
| MqlRates d[];
|
| int n=GetBars(PERIOD_D1,D1WIN,d); // completed days only, oldest first
|
| ArrayResize(c,MathMax(n,0));
|
| for(int i=0; i<n; i++)
|
| c[i]=d[i].close;
|
| if(n<InpTrendEMA) // the EMA exists from the period-th close, as in the research
|
| return(false);
|
| double s=0;
|
| for(int i=0; i<InpTrendEMA; i++)
|
| s+=c[i];
|
| double e=s/InpTrendEMA,k=2.0/(InpTrendEMA+1);
|
| for(int i=InpTrendEMA; i<n; i++)
|
| e+=k*(c[i]-e);
|
| close1=c[n-1];
|
| ema1=e;
|
| dir=(close1>e ? 1 : (close1<e ? -1 : 0));
|
| return(true);
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Signal from the last closed signal bar: +1 buy, -1 sell, 0 none |
|
| //+------------------------------------------------------------------+
|
| int Evaluate(void)
|
| {
|
| MqlRates r[];
|
| int n=GetBars(InpTF,WIN,r); // closed bars, oldest first
|
| if(n<MIN_BARS)
|
| return(0);
|
| double tr[],atr_ut[],atr_st[],atr_sl[],ut[];
|
| int st[];
|
| TrueRange(r,n,tr);
|
| Rma(tr,n,InpUTATR,atr_ut);
|
| Rma(tr,n,InpSTPeriod,atr_st);
|
| Rma(tr,n,InpStopATR,atr_sl);
|
| UTBot(r,n,atr_ut,InpUTKey,ut);
|
| Supertrend(r,n,atr_st,InpSTMult,st);
|
| int j=n-1;
|
| if(ut[j]==EMPTY_VALUE || ut[j-1]==EMPTY_VALUE || atr_sl[j]==EMPTY_VALUE)
|
| return(0);
|
| g_ut_stop=ut[j];
|
| g_ut_side=(r[j].close>ut[j] ? 1 : -1);
|
| g_st_dir=st[j];
|
| g_atr_stop=atr_sl[j];
|
| if(!DailyTrend(g_d1_dir,g_d1_close,g_d1_ema))
|
| return(0);
|
| bool ut_buy =r[j].close>ut[j] && r[j-1].close<=ut[j-1];
|
| bool ut_sell=r[j].close<ut[j] && r[j-1].close>=ut[j-1];
|
| if(ut_buy && g_d1_dir>0 && g_st_dir>0)
|
| return(1);
|
| if(ut_sell && g_d1_dir<0 && g_st_dir<0)
|
| return(-1);
|
| return(0);
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Positions |
|
| //+------------------------------------------------------------------+
|
| //--- this EA's open order on the symbol (0 = none); it stays selected
|
| int MyTicket(void)
|
| {
|
| for(int i=OrdersTotal()-1; i>=0; i--)
|
| if(MyOrderAt(i))
|
| return(OrderTicket());
|
| return(0);
|
| }
|
|
|
| string TrailKey(const int ticket) { return("GTS_TRAIL_"+IntegerToString(ticket)); }
|
|
|
| //--- watch an order of this EA until it closes: its TRD debug line, alerts, and dropping its trail variable
|
| void TradeWatch(const int t,const double sl)
|
| {
|
| for(int k=0; k<ArraySize(g_trd); k++)
|
| if(g_trd[k]==t)
|
| return;
|
| int k=ArraySize(g_trd);
|
| ArrayResize(g_trd,k+1);
|
| ArrayResize(g_trd_sl,k+1);
|
| g_trd[k]=t;
|
| g_trd_sl[k]=sl;
|
| }
|
|
|
| //--- the watched orders that have closed (by this EA, the stop or the broker): one TRD line each (debug), the
|
| //--- the trail variable dropped. Called at every tick and after every close; the survive
|
| //--- check runs on the next tick (OnTick, once a second while an order is open)
|
| void TradeLine(void)
|
| {
|
| for(int k=ArraySize(g_trd)-1; k>=0; k--)
|
| {
|
| if(!OrderSelect(g_trd[k],SELECT_BY_TICKET) || OrderCloseTime()==0)
|
| continue;
|
| if(InpDebug)
|
| PrintFormat("TRD %s %s %s %s %s %s %s %.2f %.2f",TimeToString(OrderOpenTime(),TIME_DATE|TIME_MINUTES), // MQL4: no %.*f
|
| TimeToString(OrderCloseTime(),TIME_DATE|TIME_MINUTES),OrderType()==OP_BUY ? "BUY" : "SELL",
|
| DoubleToString(OrderLots(),2),DoubleToString(OrderOpenPrice(),_Digits),DoubleToString(OrderClosePrice(),_Digits),
|
| DoubleToString(g_trd_sl[k],_Digits),OrderProfit(),OrderSwap());
|
| GlobalVariableDel(TrailKey(g_trd[k]));
|
| int last=ArraySize(g_trd)-1;
|
| g_trd[k]=g_trd[last];
|
| g_trd_sl[k]=g_trd_sl[last];
|
| ArrayResize(g_trd,last);
|
| ArrayResize(g_trd_sl,last);
|
| g_stats_dirty=true;
|
| }
|
| }
|
|
|
| bool ClosePosition(const int ticket)
|
| {
|
| if(!OpenTicket(ticket))
|
| return(true);
|
| RefreshRates();
|
| bool lng=OrderType()==OP_BUY;
|
| bool ok=OrderClose(ticket,OrderLots(),lng ? Bid_() : Ask_(),50,clrNONE);
|
| if(!ok)
|
| PrintFormat("Gold Trend Sentinel MT4: close of #%d refused, error %d",ticket,GetLastError());
|
| TradeLine();
|
| return(ok);
|
| }
|
|
|
| //--- after any fill: if this EA's position could not reach its stop without the account hitting the broker's
|
| //--- stop-out level (e.g. after a gap), close it at market rather than let the broker force-close it
|
| void SurviveCheck(void)
|
| {
|
| int t=MyTicket();
|
| if(t==0)
|
| return;
|
| double worst=AccountInfoDouble(ACCOUNT_EQUITY);
|
| bool lng=OrderType()==OP_BUY;
|
| double psl=OrderStopLoss(),cur=lng ? Bid_() : Ask_();
|
| if(psl>0 && (lng ? psl<cur : psl>cur))
|
| worst-=GAP_MULT*OrderLots()*LossPerLot(cur,psl);
|
| double so=AccountInfoDouble(ACCOUNT_MARGIN_SO_SO);
|
| double used=AccountInfoDouble(ACCOUNT_MARGIN);
|
| double floor_=AccountInfoInteger(ACCOUNT_MARGIN_SO_MODE)==ACCOUNT_STOPOUT_MODE_PERCENT ? used*so/100.0 : so;
|
| if(worst<=floor_ && ClosePosition(t))
|
| {
|
| g_sig_result="closed: balance too small for SL";
|
| Print("Gold Trend Sentinel MT4: the balance could not carry the position to its stop; closed at market.");
|
| }
|
| }
|
|
|
| //--- market order with the stop attached; "placed" or why not
|
|
|
| string OpenPosition(const bool buy)
|
| {
|
| RefreshRates();
|
| double price=buy ? Ask_() : Bid_();
|
| double dist=InpStopMult*g_atr_stop;
|
| double gap=MarketInfo(_Symbol,MODE_STOPLEVEL)*_Point;
|
| double spread=Ask_()-Bid_();
|
| if(dist<gap+spread+_Point) // never inside the broker's stop level or the spread: a stop
|
| dist=gap+spread+_Point; // on the wrong side of the bid/ask is rejected "Invalid stops"
|
| double sl=FixPrice(buy ? price-dist : price+dist);
|
| string why="";
|
| double volume=TradeVolume(price,sl,why);
|
| if(volume<=0)
|
| return(why);
|
| string why_not=Affordable(buy,volume,price,sl);
|
| if(why_not!="")
|
| return(why_not);
|
| int t=OrderSend(_Symbol,buy ? OP_BUY : OP_SELL,volume,price,50,sl,0,"GTS",InpMagic,0,clrNONE);
|
| if(t<0)
|
| {
|
| int err=GetLastError();
|
| why=err==ERR_NOT_ENOUGH_MONEY ? "not enough margin" : "error "+IntegerToString(err);
|
| PrintFormat("Gold Trend Sentinel MT4: %s %.2f lot not opened: %s",buy ? "buy" : "sell",volume,why);
|
| return("rejected: "+why);
|
| }
|
| GlobalVariableSet(TrailKey(t),dist); // the trail keeps the entry's distance, across restarts
|
| TradeWatch(t,sl);
|
| return("placed");
|
| }
|
|
|
| //--- chandelier trail, once per signal bar: best price since entry (fill and every closed bar's extreme)
|
| void TrailStop(void)
|
| {
|
| if(!InpTrail)
|
| return;
|
| int t=MyTicket();
|
| if(t==0)
|
| return;
|
| double dist=GlobalVariableCheck(TrailKey(t)) ? GlobalVariableGet(TrailKey(t)) : InpStopMult*g_atr_stop;
|
| bool lng=OrderType()==OP_BUY;
|
| double open=OrderOpenPrice(),sl=OrderStopLoss();
|
| //--- from the start of the bar the position opened in: the research counts that bar's extreme too
|
| datetime opened=OrderOpenTime();
|
| int per=PeriodSeconds(InpTF);
|
| datetime since=g_native ? iTime(_Symbol,InpTF,MathMax(iBarShift(_Symbol,InpTF,opened),0)) : NyBucket(opened,per);
|
| datetime cur=CurBar();
|
| if(since>=cur)
|
| return; // opened in the current bar: nothing closed yet
|
| MqlRates r[];
|
| int n=GetBars(InpTF,(int)((cur-since)/per)+2,r);
|
| double best=open;
|
| for(int i=0; i<n; i++)
|
| if(r[i].time>=since)
|
| best=lng ? MathMax(best,r[i].high) : MathMin(best,r[i].low+r[i].spread*_Point);
|
| double ns=FixPrice(lng ? best-dist : best+dist);
|
| if(sl>0 && (lng ? ns<=sl : ns>=sl))
|
| return; // only ever tightens
|
| RefreshRates();
|
| double bid=Bid_(),ask=Ask_();
|
| double gap=MarketInfo(_Symbol,MODE_STOPLEVEL)*_Point;
|
| double freeze=MarketInfo(_Symbol,MODE_FREEZELEVEL)*_Point;
|
| //--- the new stop is already through the price: the tested rule exits at this bar's open
|
| if(lng ? ns>=bid : ns<=ask)
|
| {
|
| if(ClosePosition(t))
|
| PrintFormat("Gold Trend Sentinel MT4: trail stop %s passed the price, position closed",DoubleToString(ns,_Digits));
|
| return;
|
| }
|
| if(lng ? bid-ns<=MathMax(gap,freeze) : ns-ask<=MathMax(gap,freeze))
|
| return; // too close for the broker now; try again next bar
|
| if(OpenTicket(t) && !OrderModify(t,OrderOpenPrice(),ns,OrderTakeProfit(),0,clrNONE))
|
| PrintFormat("Gold Trend Sentinel MT4: trail of #%d to %s refused, error %d",t,DoubleToString(ns,_Digits),GetLastError());
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Chart: the UT Bot line and signal markers |
|
| //+------------------------------------------------------------------+
|
|
|
| void DrawLevels(void)
|
| {
|
| if(!g_ui || !InpShowLevels || g_ut_stop<=0)
|
| return;
|
| string name=PREFIX+"ut";
|
| if(ObjectFind(0,name)<0)
|
| {
|
| ObjectCreate(0,name,OBJ_HLINE,0,0,g_ut_stop);
|
| ObjectSetInteger(0,name,OBJPROP_STYLE,STYLE_DOT);
|
| ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
|
| ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
|
| }
|
| ObjectSetDouble(0,name,OBJPROP_PRICE1,g_ut_stop);
|
| ObjectSetInteger(0,name,OBJPROP_COLOR,g_ut_side>0 ? UiPos() : UiNeg());
|
| ObjectSetString(0,name,OBJPROP_TOOLTIP,"UT Bot line "+DoubleToString(g_ut_stop,_Digits));
|
| }
|
|
|
| void Marker(const bool buy,const datetime t)
|
| {
|
| if(!g_ui || !InpShowLevels)
|
| return;
|
| string name=PREFIX+"m"+IntegerToString(g_markers%MARKERS);
|
| g_markers++;
|
| ObjectDelete(0,name);
|
| MqlRates r[];
|
| if(GetBars(InpTF,1,r)<1)
|
| return;
|
| double price=buy ? r[0].low : r[0].high;
|
| ObjectCreate(0,name,buy ? OBJ_ARROW_BUY : OBJ_ARROW_SELL,0,r[0].time-Shift(r[0].time),price);
|
| ObjectSetInteger(0,name,OBJPROP_COLOR,buy ? UiPos() : UiNeg());
|
| ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
|
| ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
|
| ObjectSetString(0,name,OBJPROP_TOOLTIP,(buy ? "buy signal: " : "sell signal: ")+g_sig_result);
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Status panel |
|
| //+------------------------------------------------------------------+
|
| string Dir(const int d,const string up,const string dn) { return(d>0 ? up : (d<0 ? dn : "-")); }
|
|
|
| void DrawPanel(void)
|
| {
|
| if(!g_ui)
|
| return;
|
| if(g_stats_dirty)
|
| RecalcStats();
|
| g_cursor=0;
|
| Box("bg",0,0,UI_W,g_collapsed ? UI_HEAD : MathMax(g_rows,UI_HEAD),UiBack(),UiRule());
|
| Text("title",UI_PAD,5,"GOLD TREND SENTINEL",UiText());
|
| string ed="FREE";
|
| Text("ed",UI_W-UI_PAD-18-8-UI_CHAR*4,6,ed,UiAccent(),InpFontSize-1);
|
| Button("btn",UI_W-UI_PAD-18,4,g_collapsed ? "+" : "-");
|
| if(g_collapsed)
|
| return;
|
| int pos=MyTicket();
|
| double bid=Bid_(),ask=Ask_();
|
| g_pdh=bid;
|
| g_ref_stop=InpStopMult*g_atr_stop;
|
| string state;
|
| color sc;
|
| if(!g_symbol_ok)
|
| {
|
| state="XAUUSD only - idle";
|
| sc=UiNeg();
|
| }
|
| else
|
| if(!g_market_open)
|
| {
|
| state="market closed";
|
| sc=UiDim();
|
| }
|
| else
|
| if(pos>0 && OpenTicket(pos))
|
| {
|
| state=OrderType()==OP_BUY ? "in a long trade" : "in a short trade";
|
| sc=UiAccent();
|
| }
|
| else
|
| {
|
| state="watching for a signal";
|
| sc=UiText();
|
| }
|
| int y=UI_HEAD+2;
|
| y=Section("s1",y,"STATUS");
|
| y=Row("state",y,"State",state,sc);
|
| y=Row("lot",y,"Lot",LotText(),UiText());
|
| string rt=RiskText();
|
| y=Row("risk",y,"Risk",rt,rt=="-" ? UiDim() : RiskColor(g_risk_now));
|
| y=Row("spread",y,"Spread",DoubleToString(ask-bid,_Digits),UiAccent());
|
| y=Row("next",y,"Next check",Clock(SecsToNextBar()),UiDim());
|
| y=Row("clock",y,"Candles",ClockText(),g_native ? UiDim() : UiWarn());
|
| y=Section("s2",y,"TREND FILTERS");
|
| y=Row("d1",y,"Daily trend",g_d1_ema>0 ? Dir(g_d1_dir,"UP","DOWN")+StringFormat(" %+.1f%% vs EMA",100*(g_d1_close/g_d1_ema-1)) : "-",
|
| g_d1_dir>0 ? UiPos() : (g_d1_dir<0 ? UiNeg() : UiDim()));
|
| y=Row("st",y,"Supertrend",Dir(g_st_dir,"UP","DOWN"),g_st_dir>0 ? UiPos() : (g_st_dir<0 ? UiNeg() : UiDim()));
|
| y=Row("ut",y,"UT Bot line",g_ut_stop>0 ? Px(g_ut_stop)+(g_ut_side>0 ? " price above" : " price below") : "-",UiAccent());
|
| string allow=(g_d1_dir>0 && g_st_dir>0) ? "buys only" : ((g_d1_dir<0 && g_st_dir<0) ? "sells only" : "none (filters disagree)");
|
| y=Row("allow",y,"Allowed",allow,StringFind(allow,"none")==0 ? UiDim() : UiWarn());
|
| string ls=g_sig_time>0 ? StringFormat("%s %s",g_sig_side>0 ? "BUY" : "SELL",TimeToString(g_sig_time,TIME_DATE|TIME_MINUTES)) : "none yet";
|
| y=Row("last",y,"Last signal",ls,g_sig_time>0 ? (g_sig_side>0 ? UiPos() : UiNeg()) : UiDim());
|
| y=Row("res",y,"",g_sig_result,g_sig_result=="placed" || g_sig_result=="reversed" ? UiPos() : UiDim());
|
| y=Section("s3",y,"POSITION");
|
| string pv="-",slv="-",fl="-",rr="-";
|
| color fc=UiDim();
|
| if(pos>0 && OpenTicket(pos))
|
| {
|
| bool lng=OrderType()==OP_BUY;
|
| double op=OrderOpenPrice(),sl=OrderStopLoss();
|
| double f=OrderProfit()+OrderSwap();
|
| double dist=GlobalVariableCheck(TrailKey(pos)) ? GlobalVariableGet(TrailKey(pos)) : g_ref_stop;
|
| pv=StringFormat("%s %s @ %s",lng ? "LONG" : "SHORT",DoubleToString(OrderLots(),2),Px(op));
|
| slv=Px(sl)+(sl>0 && (lng ? sl>op : sl<op) ? " locked in" : "");
|
| fl=Money(f);
|
| fc=Sign(f);
|
| if(dist>0)
|
| rr=StringFormat("%+.2fR",(lng ? bid-op : op-ask)/dist);
|
| }
|
| y=Row("pos",y,"Position",pv,pos>0 ? UiText() : UiDim());
|
| y=Row("sl",y,"Trail stop",slv,pos>0 ? UiText() : UiDim());
|
| y=Row("float",y,"Floating",fl,fc);
|
| y=Row("rnow",y,"Now",rr,pos>0 ? UiText() : UiDim());
|
| y=Section("s4",y,"RESULTS (this EA, this symbol)");
|
| string tr=g_closed>0 ? StringFormat("%d (%dW / %dL, %.0f%%)",g_closed,g_wins,g_closed-g_wins,100.0*g_wins/g_closed) : "none yet";
|
| y=Row("trades",y,"Trades",tr,g_closed>0 ? UiText() : UiDim());
|
| y=Row("net",y,"Net",Money(g_net),Sign(g_net));
|
| string pf=g_gross_loss>0 ? DoubleToString(g_gross_win/g_gross_loss,2) : (g_gross_win>0 ? "inf" : "-");
|
| y=Row("pf",y,"Profit factor",pf,g_closed>0 ? UiText() : UiDim());
|
| y=Row("today",y,"Today",Money(g_today),Sign(g_today));
|
| g_rows=y+UI_PAD-4;
|
| Box("bg",0,0,UI_W,g_rows,UiBack(),UiRule());
|
| if(g_panel_dirty)
|
| {
|
| ChartRedraw();
|
| g_panel_dirty=false;
|
| }
|
| }
|
|
|
| //+------------------------------------------------------------------+
|
| //| Expert events |
|
| //+------------------------------------------------------------------+
|
| //--- XAUUSD, with any broker prefix or suffix (XAUUSDm, XAUUSD.pro, pXAUUSD...)
|
| bool GoldSymbol(void)
|
| {
|
| string s=_Symbol;
|
| StringToUpper(s);
|
| return(StringFind(s,"XAUUSD")>=0);
|
| }
|
|
|
| int OnInit(void)
|
| {
|
| g_tick=MarketInfo(_Symbol,MODE_TICKSIZE);
|
| g_vol_min=MarketInfo(_Symbol,MODE_MINLOT);
|
| g_vol_max=MarketInfo(_Symbol,MODE_MAXLOT);
|
| g_vol_step=MarketInfo(_Symbol,MODE_LOTSTEP);
|
| g_last_bar=0;
|
| //--- live and demo charts: XAUUSD only. The Strategy Tester may test any symbol (the Market's automatic
|
| //--- validation runs EURUSD and GBPUSD on a small account, where gold's minimum lot cannot be carried safely)
|
| g_symbol_ok=GoldSymbol() || MQLInfoInteger(MQL_TESTER);
|
| if(!GoldSymbol())
|
| {
|
| string msg=MQLInfoInteger(MQL_TESTER) ?
|
| StringFormat("Gold Trend Sentinel MT4: testing %s in the Strategy Tester; on live charts it trades XAUUSD only.",_Symbol) :
|
| StringFormat("Gold Trend Sentinel MT4 works on XAUUSD charts only; it will not trade on %s.",_Symbol);
|
| Print(msg);
|
| if(!MQLInfoInteger(MQL_TESTER))
|
| Alert(msg);
|
| }
|
| ResolveClock();
|
| if(InpDebug) // one line with everything a bug report needs (no account number)
|
| PrintFormat("DBG start: Gold Trend Sentinel %s %s, %s, signals %s, server %s, %s account, digits %d, tick %s, "
|
| "lot min/step/max %s/%s/%s, stops %d, freeze %d, clock %s, terminal build %d",GTS_VERSION,
|
| "Free",_Symbol,StringSubstr(EnumToString(InpTF),7),AccountInfoString(ACCOUNT_SERVER),
|
| "MT4",_Digits,DoubleToString(g_tick,_Digits),
|
| DoubleToString(g_vol_min,2),DoubleToString(g_vol_step,2),
|
| DoubleToString(g_vol_max,2),(int)MarketInfo(_Symbol,MODE_STOPLEVEL),
|
| (int)MarketInfo(_Symbol,MODE_FREEZELEVEL),ClockText(),(int)TerminalInfoInteger(TERMINAL_BUILD));
|
| bool tester=(bool)MQLInfoInteger(MQL_TESTER);
|
| if(tester && InpClock==GTS_CLOCK_AUTO)
|
| Print("Gold Trend Sentinel MT4: the Strategy Tester can't detect the broker's clock; New York close is assumed. ",
|
| "Set 'Broker server clock' for a broker on another clock.");
|
| PrintFormat("Gold Trend Sentinel MT4: candles on the New York-close clock (%s)",ClockText());
|
| g_ui=InpShowPanel && !MQLInfoInteger(MQL_OPTIMIZATION) && (!tester || MQLInfoInteger(MQL_VISUAL_MODE));
|
| //--- drop trail variables of positions that no longer exist (closed while the EA was off)
|
| for(int i=GlobalVariablesTotal()-1; i>=0; i--)
|
| {
|
| string gv=GlobalVariableName(i);
|
| if(StringFind(gv,"GTS_TRAIL_")!=0)
|
| continue;
|
| if(!OpenTicket((int)StringToInteger(StringSubstr(gv,10))))
|
| GlobalVariableDel(gv);
|
| }
|
| //--- orders already open (a restart): watch them until they close
|
| ArrayResize(g_trd,0);
|
| ArrayResize(g_trd_sl,0);
|
| for(int i=OrdersTotal()-1; i>=0; i--)
|
| if(MyOrderAt(i))
|
| TradeWatch(OrderTicket(),OrderStopLoss());
|
| g_hist_seen=0;
|
| if(!tester)
|
| EventSetTimer(1);
|
| if(g_ui)
|
| {
|
| CacheClear();
|
| g_stats_dirty=true;
|
| DrawPanel();
|
| }
|
| return(INIT_SUCCEEDED);
|
| }
|
|
|
| void OnDeinit(const int reason)
|
| {
|
| EventKillTimer();
|
| ObjectsDeleteAll(0,PREFIX);
|
| CacheClear();
|
| ChartRedraw();
|
| }
|
|
|
| void OnTick(void)
|
| {
|
| TradeLine(); // orders closed since the last tick (stop hits)
|
| if(OrdersHistoryTotal()!=g_hist_seen)
|
| g_stats_dirty=true;
|
| if(!g_symbol_ok) // not an XAUUSD chart: never trade
|
| return;
|
| datetime bar=CurBar();
|
| static datetime guarded=0; // an open trade must stay survivable to its stop; swaps or
|
| if(OrdersTotal()>0 && TimeCurrent()!=guarded) // another position can drain the account: once a second
|
| {
|
| guarded=TimeCurrent();
|
| SurviveCheck();
|
| }
|
| if(bar==g_last_bar || (g_last_bar==0 && Bars(_Symbol,InpTF)<MIN_BARS))
|
| {
|
| if(g_ui && MQLInfoInteger(MQL_TESTER) && TimeCurrent()!=g_last_ui)
|
| {
|
| g_last_ui=TimeCurrent();
|
| DrawPanel();
|
| }
|
| return;
|
| }
|
| g_last_bar=bar;
|
| g_market_open=IsMarketOpen();
|
| if(!g_market_open)
|
| {
|
| g_last_bar=0; // try again on the first tick of the session
|
| DrawPanel();
|
| return;
|
| }
|
| int sig=Evaluate();
|
| if(InpDebug)
|
| PrintFormat("DBG %s: ut %s side %d, supertrend %d, daily %d (close %s ema %s), atr %s, signal %d",
|
| TimeToString(bar),DoubleToString(g_ut_stop,_Digits),g_ut_side,g_st_dir,g_d1_dir,
|
| DoubleToString(g_d1_close,_Digits),DoubleToString(g_d1_ema,_Digits),DoubleToString(g_atr_stop,_Digits),sig);
|
| //--- the tested order of events: exits decided at the close (reversal), then entries, then the trail
|
| int t=MyTicket();
|
| int pos=0;
|
| if(t>0)
|
| pos=OrderType()==OP_BUY ? 1 : -1;
|
| if(sig!=0)
|
| {
|
| g_sig_time=g_native ? iTime(_Symbol,InpTF,1) : bar-PeriodSeconds(InpTF)-Shift(TimeCurrent()); // server time
|
| g_sig_side=sig;
|
| if(pos==sig)
|
| {
|
| g_sig_result="already in this direction";
|
| TrailStop(); // the research tightens the trail on this bar too
|
| }
|
| else
|
| {
|
| bool reversed=false;
|
| if(pos==-sig)
|
| reversed=ClosePosition(t);
|
| if(pos==0 || reversed)
|
| {
|
| g_sig_result=OpenPosition(sig>0);
|
| if(reversed && g_sig_result=="placed")
|
| g_sig_result="reversed";
|
| }
|
| else
|
| g_sig_result="could not close the old trade";
|
| }
|
| Marker(sig>0,g_sig_time);
|
| if(InpDebug)
|
| PrintFormat("DBG %s: signal %s -> %s",TimeToString(bar),sig>0 ? "BUY" : "SELL",g_sig_result);
|
| }
|
| else
|
| TrailStop();
|
| DrawLevels();
|
| DrawPanel();
|
| }
|
|
|
| void OnTimer(void)
|
| {
|
| if(g_ui && TimeLocal()-g_cache_reset>=60)
|
| {
|
| g_cache_reset=TimeLocal();
|
| CacheClear();
|
| }
|
| DrawPanel();
|
| }
|
|
|
| void OnChartEvent(const int id,const long &lparam,const double &dparam,const string &sparam)
|
| {
|
| if(id==CHARTEVENT_OBJECT_CLICK && sparam==PREFIX+"btn")
|
| {
|
| g_collapsed=!g_collapsed;
|
| ObjectsDeleteAll(0,PREFIX+"s");
|
| for(int i=ObjectsTotal(0,0,OBJ_LABEL)-1; i>=0; i--)
|
| {
|
| string n=ObjectName(0,i,0,OBJ_LABEL);
|
| if(StringFind(n,PREFIX)==0 && n!=PREFIX+"title" && n!=PREFIX+"ed")
|
| ObjectDelete(0,n);
|
| }
|
| CacheClear();
|
| DrawPanel();
|
| ChartRedraw();
|
| }
|
| }
|
| //+------------------------------------------------------------------+
|