//+------------------------------------------------------------------+ //| RiskCore.mqh | //| Copyright 2026, Allan NiƱal | //| https://www.allanninal.dev/mql5 | //+------------------------------------------------------------------+ //| MetaTrader 4 edition of Risk Position Tool: three draggable | //| lines (entry, stop loss, take profit) and the size they imply. | //| Direction: a stop below the entry is a buy, above it a sell. | //| Lot = risk money / loss of one lot from entry to stop, rounded | //| DOWN to the lot step; below the broker minimum it says so | //| instead of rounding up. Loss per lot: tick-value maths (MQL4 | //| has no OrderCalcProfit); margin: the symbol's margin for one | //| lot (MODE_MARGINREQUIRED) x the lot. | //| Entry "at market" follows the ask (buy) or bid (sell) until | //| the entry line is dragged; then it is a pending price. | //+------------------------------------------------------------------+ // SPDX-License-Identifier: LicenseRef-PolyForm-Noncommercial-1.0.0 (see LICENSE) #ifndef RTM_CORE_MQH #define RTM_CORE_MQH //--- not "RTM_": the panel's PanelDelete removes every "RTM_" object, and the lines must survive a timeframe change #define L_ENTRY "RTL_ENTRY" #define L_SL "RTL_SL" #define L_TP "RTL_TP" enum ENUM_RTM_RISK { RTM_RISK_PCT_BAL=0, // % of balance RTM_RISK_PCT_EQ=1, // % of equity RTM_RISK_MONEY=2 // Fixed money }; struct RTMCalc { bool buy; double entry,sl,tp; double stop_pts,target_pts; // distances in points double risk_money; // what the input asks for double loss_per_lot; double lots; // rounded down to the step, capped at the maximum; 0 = below the minimum double lots_raw; double real_risk; // money lost at the stop with `lots` double real_risk_pct; // of the balance double reward; // money won at the target with `lots` double rr; // reward : risk double margin; // margin `lots` needs string why; // "" or why no lot can be given }; bool g_entry_market=true; // the entry line follows the market until it is dragged //--- the line prices, kept in memory too: the non-visual Strategy Tester has no chart objects double g_px_entry=0,g_px_sl=0,g_px_tp=0; double RTMShadow(const string name) { return(name==L_ENTRY ? g_px_entry : (name==L_SL ? g_px_sl : (name==L_TP ? g_px_tp : 0))); } void RTMSetShadow(const string name,const double p) { if(name==L_ENTRY) g_px_entry=p; else if(name==L_SL) g_px_sl=p; else if(name==L_TP) g_px_tp=p; } //--- set a line's price on the chart (when it exists) and in memory void RTMSetPrice(const string name,const double p) { if(ObjectFind(0,name)>=0) ObjectSetDouble(0,name,OBJPROP_PRICE,0,p); RTMSetShadow(name,p); } //--- the money one tick moves one lot, in the account currency. MT4 gives 0 for some CFD symbols (gold, crypto) in the //--- Strategy Tester: then tick size x contract size, in the profit currency, converted with its cross rate double RTMTickValue(const string sym) { double tv=MarketInfo(sym,MODE_TICKVALUE); if(tv>0) return(tv); double v=MarketInfo(sym,MODE_TICKSIZE)*MarketInfo(sym,MODE_LOTSIZE); string pc=SymbolInfoString(sym,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 moved from `a` to `b` by one lot: ticks x tick value (MQL4 has no OrderCalcProfit; the tick value is the //--- terminal's current one, in the account currency) double RTMMoneyPerLot(const string sym,const double a,const double b) { double tick=MarketInfo(sym,MODE_TICKSIZE),tv=RTMTickValue(sym); return(tick>0 && tv>0 ? MathAbs(a-b)/tick*tv : 0); } //--- a price rounded to the symbol's tick (0 stays 0) double RTMTickPrice(const double p) { double t=MarketInfo(_Symbol,MODE_TICKSIZE); if(p<=0) return(0); return(t>0 ? NormalizeDouble(MathRound(p/t)*t,_Digits) : NormalizeDouble(p,_Digits)); } //--- money lost from `price` to `sl` by one lot double RTMLossPerLot(const string sym,const double price,const double sl) { return(RTMMoneyPerLot(sym,price,sl)); } //--- a line's price: the chart object when there is one (it may have been dragged), else the price in memory double RTMLinePrice(const string name) { if(ObjectFind(0,name)>=0) { double p=ObjectGetDouble(0,name,OBJPROP_PRICE,0); RTMSetShadow(name,p); return(p); } return(RTMShadow(name)); } void RTMDeleteLines(void) { ObjectDelete(0,L_ENTRY); ObjectDelete(0,L_SL); ObjectDelete(0,L_TP); g_px_entry=0; g_px_sl=0; g_px_tp=0; } void RTMLine(const string name,const double price,const color clr,const ENUM_LINE_STYLE style,const string tip) { RTMSetShadow(name,price); if(ObjectFind(0,name)<0) { ObjectCreate(0,name,OBJ_HLINE,0,0,price); ObjectSetInteger(0,name,OBJPROP_SELECTABLE,true); ObjectSetInteger(0,name,OBJPROP_SELECTED,true); ObjectSetInteger(0,name,OBJPROP_WIDTH,2); ObjectSetInteger(0,name,OBJPROP_BACK,false); } ObjectSetInteger(0,name,OBJPROP_COLOR,clr); ObjectSetInteger(0,name,OBJPROP_STYLE,style); ObjectSetString(0,name,OBJPROP_TOOLTIP,tip); } //--- place the three lines: stop `stop_atr` x ATR(14) away, target `rr` x the stop distance, direction `buy` void RTMResetLines(const bool buy,const double stop_atr,const double rr,const color c_entry,const color c_sl,const color c_tp) { double atr=0; MqlRates r[]; ArraySetAsSeries(r,true); int n=CopyRates(_Symbol,_Period,1,15,r); for(int i=0; i1 ? atr/(n-1) : 100*_Point); double entry=buy ? MarketInfo(_Symbol,MODE_ASK) : MarketInfo(_Symbol,MODE_BID); if(entry<=0) entry=(n>0 ? r[0].close : 0); double d=MathMax(stop_atr*atr,10*_Point); double sl=entry+(buy ? -d : d),tp=entry+(buy ? rr*d : -rr*d); RTMDeleteLines(); RTMLine(L_ENTRY,entry,c_entry,STYLE_SOLID,"Entry (drag it for a pending order price)"); RTMLine(L_SL,sl,c_sl,STYLE_DASH,"Stop loss (drag it)"); RTMLine(L_TP,tp,c_tp,STYLE_DASH,"Take profit (drag it)"); g_entry_market=true; } //--- keep the entry line on the market price while it has not been dragged void RTMFollowMarket(void) { if(!g_entry_market || RTMLinePrice(L_ENTRY)<=0) return; bool buy=RTMLinePrice(L_SL)0 && MathAbs(p-RTMLinePrice(L_ENTRY))>_Point/2) { //--- the stop and target keep their distance from the entry double e=RTMLinePrice(L_ENTRY),s=RTMLinePrice(L_SL),t=RTMLinePrice(L_TP); RTMSetPrice(L_ENTRY,p); RTMSetPrice(L_SL,s+(p-e)); if(t>0) RTMSetPrice(L_TP,t+(p-e)); } } //--- decimals of the lot step (0.01 -> 2, 0.001 -> 3, 1 -> 0) int RTMLotDigits(void) { double step=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_STEP); int d=0; while(d<8 && step>0 && MathAbs(step*MathPow(10,d)-MathRound(step*MathPow(10,d)))>1e-9) d++; return(d); } //--- the size the lines imply for `risk` (percent or money, per mode) bool RTMCompute(RTMCalc &c,const ENUM_RTM_RISK mode,const double risk) { ZeroMemory(c); c.why=""; // ZeroMemory leaves a NULL string, which is not equal to "" //--- the prices an order is sent with (rounded to the tick), so the size is measured on the stop the broker gets c.entry=RTMTickPrice(RTMLinePrice(L_ENTRY)); c.sl=RTMTickPrice(RTMLinePrice(L_SL)); c.tp=RTMTickPrice(RTMLinePrice(L_TP)); if(c.entry<=0 || c.sl<=0 || c.entry==c.sl) { c.why="place entry and stop"; return(false); } c.buy=c.sl0 ? _Point : 1; c.stop_pts=MathAbs(c.entry-c.sl)/pt; c.target_pts=(c.tp>0 ? MathAbs(c.tp-c.entry)/pt : 0); double bal=AccountInfoDouble(ACCOUNT_BALANCE); c.risk_money=(mode==RTM_RISK_MONEY ? risk : (mode==RTM_RISK_PCT_EQ ? AccountInfoDouble(ACCOUNT_EQUITY) : bal)*risk/100.0); c.loss_per_lot=RTMLossPerLot(_Symbol,c.entry,c.sl); if(c.loss_per_lot<=0) { c.why="no tick value for symbol"; return(false); } double step=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_STEP),vmin=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN); double vmax=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MAX); c.lots_raw=c.risk_money/c.loss_per_lot; double v=(step>0 ? MathFloor(c.lots_raw/step+1e-9)*step : c.lots_raw); v=NormalizeDouble(MathMin(v,vmax),8); if(v0 ? c.lots : vmin); c.real_risk=use*c.loss_per_lot; c.real_risk_pct=(bal>0 ? 100.0*c.real_risk/bal : 0); if(c.tp>0) c.reward=use*RTMMoneyPerLot(_Symbol,c.entry,c.tp); c.rr=(c.stop_pts>0 && c.target_pts>0 ? c.target_pts/c.stop_pts : 0); c.margin=use*MarketInfo(_Symbol,MODE_MARGINREQUIRED); if(c.lots>0 && c.margin>AccountFreeMargin()) c.why="not enough free margin"; return(c.lots>0 && c.why==""); } #endif //+------------------------------------------------------------------+