RiskManagement/RiskManagement.mq5

1806 lines
63 KiB
MQL5

//+------------------------------------------------------------------+
//| EnterpriseOrderPanel_Minimal.mq5 |
//| Minimal MT5 panel: draggable window + minimize/close + buttons |
//+------------------------------------------------------------------+
#property strict
#property version "1.00"
//====================================================================
// Inputs
//====================================================================
input double InpUIScale = 1.00;
input int InpStartX = 20;
input int InpStartY = 30;
input ulong InpMagicNumber=270927;
input uint InpDeviationPoints=20;
//====================================================================
// Geometry
//====================================================================
#define BASE_W 270
#define HEADER_H 26
#define BODY_H 210
#define OUTER_PAD 6
#define BUTTON_H 24
//====================================================================
// Theme
//====================================================================
color C_SHADOW = C'5,8,12';
color C_FRAME = C'42,52,66';
color C_BODY = C'12,19,28';
color C_HEADER = C'17,27,39';
color C_HEADER_TOP = C'24,38,53';
color C_TEXT = C'236,241,248';
color C_TEXT2 = C'196,208,224';
color C_MUTED = C'129,148,173';
color C_ACCENT = C'30,132,255';
color C_ACCENT_DARK = C'20,104,211';
color C_BTN = C'22,31,43';
color C_BTN_PRESS = C'34,47,63';
color C_CLOSE = C'178,47,58';
color C_CLOSE_PRESS = C'204,58,69';
//====================================================================
// State
//====================================================================
double g_scale=1.0;
int g_x=20;
int g_y=30;
bool g_minimized=false;
enum ERiskUnit { RISK_DOLLAR=0, RISK_PERCENT, RISK_LOT };
ERiskUnit g_risk_unit=RISK_DOLLAR;
double g_risk_value=0.0;
string g_risk_text="0";
string g_dollar_text="0",g_percent_text="1";
string g_lot_text="0.01";
string N_LOT_BG="MIN_LOT_BG",N_LOT_TX="MIN_LOT_TX";
bool g_state_ready=false;
bool g_restore_stop=false,g_restore_entry=false;
double g_saved_stop=0.0,g_saved_entry=0.0;
string N_STATE="";
string N_EXEC_LOCK="";
bool g_executing=false;
bool g_prev_left=false;
bool g_dragging=false;
int g_drag_dx=0;
int g_drag_dy=0;
bool g_chart_scroll_before=true;
bool g_stop_initialized=false;
double g_stop_price=0.0;
string g_stop_caption="";
int g_stop_label_x=-1,g_stop_label_y=-1;
bool g_stop_label_visible=false;
enum EEntryMode { ENTRY_MARKET=0, ENTRY_LIMIT };
EEntryMode g_entry_mode=ENTRY_MARKET;
bool g_entry_initialized=false;
double g_entry_price=0.0;
string g_entry_caption="";
int g_entry_label_x=-1,g_entry_label_y=-1;
bool g_entry_label_visible=false;
bool g_tp_rr=true,g_tp_initialized=false;
double g_tp_reward=3.0,g_tp_price=0.0,g_tp_last_set=0.0,g_calc_volume=0.0;
string g_tp_caption="";
int g_tp_x=-1,g_tp_y=-1;
bool g_tp_visible=false;
string N_TP_LINE="MIN_TP_LINE",N_TP_PRICE="MIN_TP_PRICE";
string N_TP_EDIT="MIN_TP_EDIT",N_TP_TITLE="MIN_TP_TITLE";
string N_TP_RR_BG="MIN_TP_RR_BG",N_TP_RR_TX="MIN_TP_RR_TX";
string N_TP_VALUE_BG="MIN_TP_VALUE_BG",N_TP_VALUE_TX="MIN_TP_VALUE_TX";
int g_steps=1;
bool g_lines_cleared=false;
string N_CLEAR_BG="MIN_CLEAR_BG",N_CLEAR_TX="MIN_CLEAR_TX";
string N_STEPS_TITLE="MIN_STEPS_TITLE";
string N_DAY_TITLE="MIN_DAY_TITLE",N_DAY_VALUE="MIN_DAY_VALUE";
ulong g_day_refresh=0;
string N_STEPS_BG[3]={"MIN_STEPS_1_BG","MIN_STEPS_2_BG","MIN_STEPS_3_BG"};
string N_STEPS_TX[3]={"MIN_STEPS_1_TX","MIN_STEPS_2_TX","MIN_STEPS_3_TX"};
string N_STAGE_LINE[2]={"MIN_ENTRY_2","MIN_ENTRY_3"};
string N_STAGE_LABEL[2]={"MIN_ENTRY_2_PRICE","MIN_ENTRY_3_PRICE"};
double g_stage_price[2]={0.0,0.0};
string g_stage_caption[2]={"",""};
int g_stage_x[2]={-1,-1},g_stage_y[2]={-1,-1};
bool g_stage_visible[2]={false,false};
//====================================================================
// Object names
//====================================================================
string N_SHADOW ="MIN_SHADOW";
string N_FRAME ="MIN_FRAME";
string N_BODY ="MIN_BODY";
string N_HEADER ="MIN_HEADER";
string N_TOPLINE ="MIN_TOPLINE";
string N_TITLE ="MIN_TITLE";
string N_SUBTITLE ="MIN_SUBTITLE";
string N_MIN_BG ="MIN_MIN_BG";
string N_MIN_TX ="MIN_MIN_TX";
string N_CLOSE_BG ="MIN_CLOSE_BG";
string N_CLOSE_TX ="MIN_CLOSE_TX";
string N_LEFT_BG ="MIN_LEFT_BG";
string N_LEFT_TX ="MIN_LEFT_TX";
string N_RIGHT_BG ="MIN_RIGHT_BG";
string N_RIGHT_TX ="MIN_RIGHT_TX";
string N_RISK_EDIT="MIN_RISK_EDIT";
string N_DOLLAR_BG="MIN_DOLLAR_BG";
string N_DOLLAR_TX="MIN_DOLLAR_TX";
string N_PERCENT_BG="MIN_PERCENT_BG";
string N_PERCENT_TX="MIN_PERCENT_TX";
string N_STOP_LINE="MIN_STOP_LINE";
string N_STOP_PRICE="MIN_STOP_PRICE";
string N_ENTRY_LINE="MIN_ENTRY_LINE";
string N_ENTRY_PRICE="MIN_ENTRY_PRICE";
string N_MARKET_BG="MIN_MARKET_BG";
string N_MARKET_TX="MIN_MARKET_TX";
string N_LIMIT_BG="MIN_LIMIT_BG";
string N_LIMIT_TX="MIN_LIMIT_TX";
//====================================================================
// Press targets
//====================================================================
enum EPressTarget
{
PRESS_NONE=0,
PRESS_MIN,
PRESS_CLOSE,
PRESS_HEADER,
PRESS_LEFT,
PRESS_RIGHT
};
EPressTarget g_pressed=PRESS_NONE;
//====================================================================
// Helpers
//====================================================================
int S(double v)
{
return (int)MathRound(v*g_scale);
}
int FS(double v)
{
int n=(int)MathRound((v+1.0)*g_scale);
return n<7 ? 7 : n;
}
void Del(string name)
{
if(ObjectFind(0,name)>=0)
ObjectDelete(0,name);
}
bool Rect(string name,int x,int y,int w,int h,color bg,color edge,long z=1)
{
if(!ObjectCreate(0,name,OBJ_RECTANGLE_LABEL,0,0,0))
return false;
ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_UPPER);
ObjectSetInteger(0,name,OBJPROP_XDISTANCE,x);
ObjectSetInteger(0,name,OBJPROP_YDISTANCE,y);
ObjectSetInteger(0,name,OBJPROP_XSIZE,w);
ObjectSetInteger(0,name,OBJPROP_YSIZE,h);
ObjectSetInteger(0,name,OBJPROP_BGCOLOR,bg);
ObjectSetInteger(0,name,OBJPROP_COLOR,edge);
ObjectSetInteger(0,name,OBJPROP_BORDER_TYPE,BORDER_FLAT);
ObjectSetInteger(0,name,OBJPROP_WIDTH,1);
ObjectSetInteger(0,name,OBJPROP_BACK,false);
ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
ObjectSetInteger(0,name,OBJPROP_ZORDER,z);
return true;
}
bool Label(string name,string text,int x,int y,int font_size,color clr,
string font="Segoe UI",long z=20)
{
if(!ObjectCreate(0,name,OBJ_LABEL,0,0,0))
return false;
ObjectSetInteger(0,name,OBJPROP_CORNER,CORNER_LEFT_UPPER);
ObjectSetInteger(0,name,OBJPROP_XDISTANCE,x);
ObjectSetInteger(0,name,OBJPROP_YDISTANCE,y);
ObjectSetInteger(0,name,OBJPROP_COLOR,clr);
ObjectSetInteger(0,name,OBJPROP_FONTSIZE,font_size);
ObjectSetString(0,name,OBJPROP_FONT,font);
ObjectSetString(0,name,OBJPROP_TEXT,text);
// Chart-level captions use z=6; panel labels stay in the foreground.
// ZORDER controls mouse priority, while BACK controls the drawing layer.
ObjectSetInteger(0,name,OBJPROP_BACK,z<10);
ObjectSetInteger(0,name,OBJPROP_SELECTABLE,false);
ObjectSetInteger(0,name,OBJPROP_HIDDEN,true);
ObjectSetInteger(0,name,OBJPROP_ZORDER,z);
return true;
}
void XYWH(string name,int x,int y,int w=-1,int h=-1)
{
if(ObjectFind(0,name)<0)
return;
ObjectSetInteger(0,name,OBJPROP_XDISTANCE,x);
ObjectSetInteger(0,name,OBJPROP_YDISTANCE,y);
if(w>=0) ObjectSetInteger(0,name,OBJPROP_XSIZE,w);
if(h>=0) ObjectSetInteger(0,name,OBJPROP_YSIZE,h);
}
void XY(string name,int x,int y)
{
XYWH(name,x,y);
}
void CenterText(string name,int x,int y,int w,int h)
{
if(ObjectFind(0,name)<0)
return;
ObjectSetInteger(0,name,OBJPROP_ANCHOR,ANCHOR_CENTER);
XY(name,x+w/2,y+h/2);
}
void Visible(string name,bool visible)
{
if(ObjectFind(0,name)<0)
return;
ObjectSetInteger(
0,
name,
OBJPROP_TIMEFRAMES,
visible ? OBJ_ALL_PERIODS : OBJ_NO_PERIODS
);
}
void RectStyle(string name,color bg,color edge)
{
if(ObjectFind(0,name)<0)
return;
ObjectSetInteger(0,name,OBJPROP_BGCOLOR,bg);
ObjectSetInteger(0,name,OBJPROP_COLOR,edge);
}
bool Hit(int px,int py,int x,int y,int w,int h)
{
return (px>=x && px<x+w && py>=y && py<y+h);
}
bool HitObject(string name,int mx,int my)
{
if(ObjectFind(0,name)<0)
return false;
if(ObjectGetInteger(0,name,OBJPROP_TIMEFRAMES)==OBJ_NO_PERIODS)
return false;
return Hit(
mx,
my,
(int)ObjectGetInteger(0,name,OBJPROP_XDISTANCE),
(int)ObjectGetInteger(0,name,OBJPROP_YDISTANCE),
(int)ObjectGetInteger(0,name,OBJPROP_XSIZE),
(int)ObjectGetInteger(0,name,OBJPROP_YSIZE)
);
}
//====================================================================
// Stop line
//====================================================================
double StopPrice(double price)
{
double step=SymbolInfoDouble(_Symbol,SYMBOL_TRADE_TICK_SIZE);
if(step>0.0) price=MathRound(price/step)*step;
return NormalizeDouble(price,_Digits);
}
void UpdatePriceLabel(string line,string label,string prefix,bool below,
double &line_price,string &last_caption,
int &last_x,int &last_y,bool &last_visible)
{
bool exists=(ObjectFind(0,line)>=0);
bool show=false;
int x=0,y=0;
if(exists)
{
line_price=ObjectGetDouble(0,line,OBJPROP_PRICE);
datetime time=0;
double price=0.0;
int window=0,unused_x=0;
if(ChartXYToTimePrice(0,0,0,window,time,price) &&
ChartTimePriceToXY(0,0,time,line_price,unused_x,y))
{
int height=(int)ChartGetInteger(0,CHART_HEIGHT_IN_PIXELS,0);
x=(int)ChartGetInteger(0,CHART_WIDTH_IN_PIXELS,0)-S(12);
show=(y>=0 && y<height);
y=below ? (int)MathMax(0,MathMin(height-S(18),y+S(3)))
: (int)MathMax(S(18),y-S(3));
}
}
bool changed=false;
if(show!=last_visible)
{
Visible(label,show);
last_visible=show;
changed=true;
}
if(show)
{
string caption=prefix+" "+DoubleToString(line_price,_Digits);
if(caption!=last_caption)
{
ObjectSetString(0,label,OBJPROP_TEXT,caption);
last_caption=caption;
changed=true;
}
if(x!=last_x || y!=last_y)
{
XY(label,x,y);
last_x=x;
last_y=y;
changed=true;
}
}
if(changed) ChartRedraw();
}
void UpdateStopLabel()
{
if(g_stop_initialized)
UpdatePriceLabel(N_STOP_LINE,N_STOP_PRICE,"SL | "+StopLossText()+" |",false,g_stop_price,
g_stop_caption,g_stop_label_x,g_stop_label_y,g_stop_label_visible);
}
double CurrentChartPrice()
{
MqlTick tick;
if(!SymbolInfoTick(_Symbol,tick)) return 0.0;
// Match the price source used to initialize the stop line.
if(tick.bid>0.0) return tick.bid;
if(tick.last>0.0) return tick.last;
return tick.ask;
}
void SaveState()
{
if(!g_state_ready) return;
bool has_stop=(ObjectFind(0,N_STOP_LINE)>=0);
bool has_entry=(ObjectFind(0,N_ENTRY_LINE)>=0);
if(has_stop) { g_saved_stop=ObjectGetDouble(0,N_STOP_LINE,OBJPROP_PRICE); g_restore_stop=true; }
if(has_entry) { g_saved_entry=ObjectGetDouble(0,N_ENTRY_LINE,OBJPROP_PRICE); g_restore_entry=true; }
for(int i=0;i<2;i++)
if(ObjectFind(0,N_STAGE_LINE[i])>=0)
g_stage_price[i]=ObjectGetDouble(0,N_STAGE_LINE[i],OBJPROP_PRICE);
string state=IntegerToString(g_x)+"|"+IntegerToString(g_y)+"|"+
IntegerToString((int)g_minimized)+"|"+IntegerToString((int)g_risk_unit)+"|"+
IntegerToString((int)g_entry_mode)+"|"+g_dollar_text+"|"+g_percent_text+"|"+
IntegerToString((int)g_restore_stop)+"|"+DoubleToString(g_saved_stop,16)+"|"+
IntegerToString((int)g_restore_entry)+"|"+DoubleToString(g_saved_entry,16)+"|"+
IntegerToString((int)g_tp_rr)+"|"+DoubleToString(g_tp_reward,8)+"|"+DoubleToString(g_tp_price,16)+"|"+
IntegerToString(g_steps)+"|"+DoubleToString(g_stage_price[0],16)+"|"+DoubleToString(g_stage_price[1],16)+"|"+
IntegerToString((int)g_lines_cleared)+"|"+g_lot_text;
if(ObjectFind(0,N_STATE)<0)
{
if(!ObjectCreate(0,N_STATE,OBJ_LABEL,0,0,0)) return;
ObjectSetInteger(0,N_STATE,OBJPROP_TIMEFRAMES,OBJ_NO_PERIODS);
ObjectSetInteger(0,N_STATE,OBJPROP_HIDDEN,true);
ObjectSetInteger(0,N_STATE,OBJPROP_SELECTABLE,false);
}
if(ObjectGetString(0,N_STATE,OBJPROP_TEXT)!=state)
ObjectSetString(0,N_STATE,OBJPROP_TEXT,state);
}
void LoadState()
{
// This invisible chart object is deliberately excluded from DeleteAll().
// It survives timeframe/input reinitialization and belongs to this chart only.
if(ObjectFind(0,N_STATE)<0) return;
string fields[];
int count=StringSplit(ObjectGetString(0,N_STATE,OBJPROP_TEXT),'|',fields);
if(count!=11 && count!=14 && count!=17 && count!=18 && count!=19) return;
g_x=(int)StringToInteger(fields[0]);
g_y=(int)StringToInteger(fields[1]);
g_minimized=(StringToInteger(fields[2])!=0);
g_risk_unit=StringToInteger(fields[3])==1 ? RISK_PERCENT : RISK_DOLLAR;
if(count>=19 && StringToInteger(fields[3])==2) g_risk_unit=RISK_LOT;
g_entry_mode=StringToInteger(fields[4])==1 ? ENTRY_LIMIT : ENTRY_MARKET;
g_dollar_text=fields[5];
g_percent_text=fields[6];
g_restore_stop=(StringToInteger(fields[7])!=0);
g_saved_stop=StringToDouble(fields[8]);
g_restore_entry=(StringToInteger(fields[9])!=0);
g_saved_entry=StringToDouble(fields[10]);
if(count>=14)
{
g_tp_rr=(StringToInteger(fields[11])!=0);
g_tp_reward=StringToDouble(fields[12]);
g_tp_price=StringToDouble(fields[13]);
if(g_tp_reward<=0.0) g_tp_reward=3.0;
}
if(count>=17)
{
g_steps=(int)MathMax(1,MathMin(3,StringToInteger(fields[14])));
g_stage_price[0]=StringToDouble(fields[15]);
g_stage_price[1]=StringToDouble(fields[16]);
}
if(count>=18) g_lines_cleared=(StringToInteger(fields[17])!=0);
if(count>=19) g_lot_text=fields[18];
}
void ClearLines()
{
CommitRiskValue();
CommitTP();
SaveState();
g_lines_cleared=true;
Del(N_STOP_LINE); Del(N_STOP_PRICE);
Del(N_ENTRY_LINE); Del(N_ENTRY_PRICE);
Del(N_TP_LINE); Del(N_TP_PRICE);
for(int i=0;i<2;i++)
{
Del(N_STAGE_LINE[i]); Del(N_STAGE_LABEL[i]);
g_stage_caption[i]=""; g_stage_x[i]=-1; g_stage_y[i]=-1; g_stage_visible[i]=false;
}
g_stop_initialized=false; g_entry_initialized=false; g_tp_initialized=false;
g_stop_caption=""; g_entry_caption=""; g_tp_caption="";
g_stop_label_x=-1; g_stop_label_y=-1;
g_entry_label_x=-1; g_entry_label_y=-1;
g_tp_x=-1; g_tp_y=-1;
g_stop_label_visible=false; g_entry_label_visible=false; g_tp_visible=false;
SaveState();
ChartRedraw();
}
void SetDefaults()
{
g_x=InpStartX;
g_y=InpStartY;
g_minimized=false;
g_risk_unit=RISK_DOLLAR;
g_dollar_text=DoubleToString(AccountInfoDouble(ACCOUNT_BALANCE)*0.01,2);
g_percent_text="1";
g_lot_text=DoubleToString(SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN),8);
g_risk_text=g_dollar_text;
g_risk_value=StringToDouble(g_risk_text);
g_entry_mode=ENTRY_MARKET;
g_steps=1;
g_restore_stop=false;
g_restore_entry=false;
g_saved_stop=0.0;
g_saved_entry=0.0;
g_stop_price=0.0;
g_entry_price=0.0;
for(int i=0;i<2;i++) g_stage_price[i]=0.0;
g_tp_rr=true;
g_tp_reward=3.0;
g_tp_price=0.0;
g_tp_last_set=0.0;
g_calc_volume=0.0;
g_lines_cleared=false;
}
void ResetAll()
{
// Keep the panel and its inputs; only rebuild the chart levels.
ClearLines();
Del(N_EXEC_LOCK);
g_restore_stop=false;
g_restore_entry=false;
g_saved_stop=0.0;
g_saved_entry=0.0;
g_stop_price=0.0;
g_entry_price=0.0;
for(int i=0;i<2;i++) g_stage_price[i]=0.0;
g_tp_price=0.0;
g_tp_last_set=0.0;
g_lines_cleared=false;
ObjectSetString(0,N_RIGHT_TX,OBJPROP_TEXT,"EXECUTE");
ObjectSetString(0,N_RIGHT_BG,OBJPROP_TOOLTIP,"Execute the current trading plan");
CreateStopLine();
RefreshEntry();
TPControls();
SaveState();
ChartRedraw();
}
void SetRiskUnit(ERiskUnit unit)
{
CommitRiskValue();
g_risk_unit=unit;
g_risk_text=unit==RISK_DOLLAR ? g_dollar_text : (unit==RISK_PERCENT ? g_percent_text : g_lot_text);
g_risk_value=StringToDouble(g_risk_text);
ObjectSetString(0,N_RISK_EDIT,OBJPROP_TEXT,g_risk_text);
UpdateRiskUnit();
SaveState();
}
string EntryDirection(color &line_color)
{
line_color=C_MUTED;
bool market=(g_entry_mode==ENTRY_MARKET);
string neutral=market ? "MARKET -" : "PENDING -";
if(ObjectFind(0,N_ENTRY_LINE)<0 || ObjectFind(0,N_STOP_LINE)<0) return neutral;
double current=CurrentChartPrice();
if(current<=0.0) return neutral;
double entry=market ? current : ObjectGetDouble(0,N_ENTRY_LINE,OBJPROP_PRICE);
double stop=ObjectGetDouble(0,N_STOP_LINE,OBJPROP_PRICE);
// Stop versus entry determines side; entry versus market determines order type.
double distance=entry-stop;
if(MathAbs(distance)<_Point*0.5) return neutral;
bool buy=(distance>0.0);
line_color=buy ? C'46,196,145' : C'232,103,113';
if(market) return buy ? "BUY MARKET" : "SELL MARKET";
double offset=entry-current;
if(MathAbs(offset)<_Point*0.5)
return buy ? "BUY PENDING" : "SELL PENDING";
if(buy) return offset>0.0 ? "BUY STOP" : "BUY LIMIT";
return offset<0.0 ? "SELL STOP" : "SELL LIMIT";
}
void SetTextIfChanged(string name,string value)
{
if(ObjectGetString(0,name,OBJPROP_TEXT)!=value)
ObjectSetString(0,name,OBJPROP_TEXT,value);
}
string PositionVolumeText(double &estimated_loss,string line="")
{
if(line=="") line=N_ENTRY_LINE;
estimated_loss=-1.0;
g_calc_volume=0.0;
if(ObjectFind(0,line)<0 || ObjectFind(0,N_STOP_LINE)<0) return "Lots: --";
double entry=ObjectGetDouble(0,line,OBJPROP_PRICE);
double stop=ObjectGetDouble(0,N_STOP_LINE,OBJPROP_PRICE);
if(MathAbs(entry-stop)<_Point*0.5) return "Lots: --";
bool buy=(entry>stop);
if(line!=N_ENTRY_LINE && buy!=(ObjectGetDouble(0,N_ENTRY_LINE,OBJPROP_PRICE)>stop))
return "Lots: -- (SL side)";
if(g_entry_mode==ENTRY_MARKET && line==N_ENTRY_LINE)
{
MqlTick tick;
if(!SymbolInfoTick(_Symbol,tick)) return "Lots: --";
entry=buy ? tick.ask : tick.bid;
}
if(entry<=0.0 || stop<=0.0) return "Lots: --";
// OrderCalcProfit returns money in account currency, including conversion.
double risk=g_risk_unit==RISK_PERCENT
? AccountInfoDouble(ACCOUNT_BALANCE)*g_risk_value/100.0 : g_risk_value;
double minimum=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MIN);
double maximum=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_MAX);
double step=SymbolInfoDouble(_Symbol,SYMBOL_VOLUME_STEP);
if(!MathIsValidNumber(risk) || risk<0.0 || minimum<=0.0 ||
maximum<minimum || step<=0.0) return "Lots: --";
int digits=0;
while(digits<8 && MathAbs(step-NormalizeDouble(step,digits))>1e-10) digits++;
double loss=0.0;
if(!OrderCalcProfit(buy ? ORDER_TYPE_BUY : ORDER_TYPE_SELL,
_Symbol,minimum,entry,stop,loss) ||
!MathIsValidNumber(loss) || loss>=0.0) return "Lots: --";
double raw=g_risk_unit==RISK_LOT ? MathMax(minimum,g_risk_value) : risk/(-loss)*minimum;
if(!MathIsValidNumber(raw)) return "Lots: --";
// Round down so the displayed volume does not exceed the requested risk.
double volume=NormalizeDouble(MathFloor(MathMin(raw,maximum)/step)*step,digits);
if(volume<minimum)
{
estimated_loss=0.0;
return "Lots: "+DoubleToString(0.0,digits)+" (< min)";
}
estimated_loss=(-loss)*volume/minimum;
g_calc_volume=volume;
return "Lots: "+DoubleToString(volume,digits);
}
string MoneyUSD(double loss,string title)
{
string currency=AccountInfoString(ACCOUNT_CURRENCY);
if(currency!="USD" && loss!=0.0)
{
// Convert using an available direct/inverse quote, including broker suffixes.
bool converted=false;
for(int i=0;i<SymbolsTotal(false);i++)
{
string symbol=SymbolName(i,false);
string base=SymbolInfoString(symbol,SYMBOL_CURRENCY_BASE);
string quote=SymbolInfoString(symbol,SYMBOL_CURRENCY_PROFIT);
bool direct=(base==currency && quote=="USD");
bool inverse=(base=="USD" && quote==currency);
if(!direct && !inverse) continue;
if(!SymbolSelect(symbol,true)) continue;
MqlTick tick;
if(!SymbolInfoTick(symbol,tick)) continue;
if(direct && tick.bid>0.0) { loss*=tick.bid; converted=true; break; }
if(inverse && tick.ask>0.0) { loss/=tick.ask; converted=true; break; }
}
if(!converted) return title+" USD: --";
}
return title+": $"+DoubleToString(loss,2);
}
string StopLossText()
{
double loss=0.0;
for(int i=0;i<g_steps;i++)
{
double part;
PositionVolumeText(part,i==0 ? N_ENTRY_LINE : N_STAGE_LINE[i-1]);
if(part<0.0) return "Total loss: --";
loss+=part;
}
return MoneyUSD(loss,g_steps>1 ? "Total loss" : "Loss");
}
void StepControls()
{
for(int i=0;i<3;i++)
{
bool active=(g_steps==i+1);
RectStyle(N_STEPS_BG[i],active ? C_ACCENT : C_BTN,active ? C_ACCENT : C_FRAME);
ObjectSetInteger(0,N_STEPS_TX[i],OBJPROP_COLOR,active ? C_TEXT : C_MUTED);
}
}
void RefreshStages()
{
if(ObjectFind(0,N_ENTRY_LINE)<0 || ObjectFind(0,N_STOP_LINE)<0) return;
double first=ObjectGetDouble(0,N_ENTRY_LINE,OBJPROP_PRICE);
double stop=ObjectGetDouble(0,N_STOP_LINE,OBJPROP_PRICE);
double current=CurrentChartPrice();
for(int i=0;i<2;i++)
{
if(i+2>g_steps)
{
if(ObjectFind(0,N_STAGE_LINE[i])>=0)
g_stage_price[i]=ObjectGetDouble(0,N_STAGE_LINE[i],OBJPROP_PRICE);
Del(N_STAGE_LINE[i]); Del(N_STAGE_LABEL[i]);
g_stage_caption[i]=""; g_stage_x[i]=-1; g_stage_y[i]=-1; g_stage_visible[i]=false;
continue;
}
if(ObjectFind(0,N_STAGE_LINE[i])<0)
{
if(g_stage_price[i]<=0.0)
g_stage_price[i]=StopPrice(first+(stop-first)*(i+1)/3.0);
if(!ObjectCreate(0,N_STAGE_LINE[i],OBJ_HLINE,0,0,g_stage_price[i])) continue;
ObjectSetInteger(0,N_STAGE_LINE[i],OBJPROP_BACK,true);
ObjectSetInteger(0,N_STAGE_LINE[i],OBJPROP_STYLE,STYLE_DASHDOT);
ObjectSetInteger(0,N_STAGE_LINE[i],OBJPROP_SELECTABLE,true);
ObjectSetInteger(0,N_STAGE_LINE[i],OBJPROP_SELECTED,true);
ObjectSetInteger(0,N_STAGE_LINE[i],OBJPROP_HIDDEN,true);
ObjectSetInteger(0,N_STAGE_LINE[i],OBJPROP_ZORDER,5);
if(!Label(N_STAGE_LABEL[i],"",0,0,FS(8),C_ACCENT,"Segoe UI Semibold",6))
{ Del(N_STAGE_LINE[i]); continue; }
ObjectSetInteger(0,N_STAGE_LABEL[i],OBJPROP_ANCHOR,ANCHOR_RIGHT_UPPER);
Visible(N_STAGE_LABEL[i],false);
}
double entry=ObjectGetDouble(0,N_STAGE_LINE[i],OBJPROP_PRICE);
bool buy=(first>stop);
string direction=buy ? (entry>current ? "BUY STOP" : "BUY LIMIT")
: (entry<current ? "SELL STOP" : "SELL LIMIT");
if(MathAbs(entry-current)<_Point*0.5) direction=buy ? "BUY PENDING" : "SELL PENDING";
double loss;
string volume=PositionVolumeText(loss,N_STAGE_LINE[i]);
color clr=loss<0.0 ? C_MUTED : (buy ? C'46,196,145' : C'232,103,113');
ObjectSetInteger(0,N_STAGE_LINE[i],OBJPROP_COLOR,clr);
ObjectSetInteger(0,N_STAGE_LABEL[i],OBJPROP_COLOR,clr);
string caption="#"+IntegerToString(i+2)+" "+direction+" | "+volume+" |";
UpdatePriceLabel(N_STAGE_LINE[i],N_STAGE_LABEL[i],caption,true,g_stage_price[i],
g_stage_caption[i],g_stage_x[i],g_stage_y[i],g_stage_visible[i]);
}
}
void TPControls(bool text=true)
{
RectStyle(N_TP_RR_BG,g_tp_rr ? C_ACCENT : C_BTN,g_tp_rr ? C_ACCENT : C_FRAME);
RectStyle(N_TP_VALUE_BG,g_tp_rr ? C_BTN : C_ACCENT,g_tp_rr ? C_FRAME : C_ACCENT);
ObjectSetInteger(0,N_TP_RR_TX,OBJPROP_COLOR,g_tp_rr ? C_TEXT : C_MUTED);
ObjectSetInteger(0,N_TP_VALUE_TX,OBJPROP_COLOR,g_tp_rr ? C_MUTED : C_TEXT);
if(text) ObjectSetString(0,N_TP_EDIT,OBJPROP_TEXT,
g_tp_rr ? DoubleToString(g_tp_reward,2) : DoubleToString(g_tp_price,_Digits));
}
void RefreshTP()
{
if(g_lines_cleared) return;
if(ObjectFind(0,N_ENTRY_LINE)<0 || ObjectFind(0,N_STOP_LINE)<0) return;
// Native dragging changes the object price before OBJECT_DRAG is delivered.
if(g_tp_initialized && ObjectFind(0,N_TP_LINE)>=0)
{
double moved=ObjectGetDouble(0,N_TP_LINE,OBJPROP_PRICE);
if(MathAbs(moved-g_tp_last_set)>_Point*0.1)
{
g_tp_rr=false;
g_tp_price=moved;
g_tp_last_set=moved;
TPControls();
}
}
double entry=ObjectGetDouble(0,N_ENTRY_LINE,OBJPROP_PRICE);
double stop=ObjectGetDouble(0,N_STOP_LINE,OBJPROP_PRICE);
bool buy=entry>stop;
if(g_entry_mode==ENTRY_MARKET)
{
MqlTick tick;
if(!SymbolInfoTick(_Symbol,tick)) return;
entry=buy ? tick.ask : tick.bid;
}
if(entry<=0.0 || MathAbs(entry-stop)<_Point*0.5) return;
bool seed_price=(!g_tp_rr && g_tp_price<=0.0);
double target=(g_tp_rr || seed_price) ? StopPrice(entry+(entry-stop)*g_tp_reward) : g_tp_price;
if(!MathIsValidNumber(target) || target<=0.0) return;
if(!g_tp_initialized)
{
if(!ObjectCreate(0,N_TP_LINE,OBJ_HLINE,0,0,target)) return;
ObjectSetInteger(0,N_TP_LINE,OBJPROP_BACK,true);
ObjectSetInteger(0,N_TP_LINE,OBJPROP_COLOR,C'46,196,145');
ObjectSetInteger(0,N_TP_LINE,OBJPROP_STYLE,STYLE_DASH);
ObjectSetInteger(0,N_TP_LINE,OBJPROP_SELECTABLE,true);
ObjectSetInteger(0,N_TP_LINE,OBJPROP_SELECTED,true);
ObjectSetInteger(0,N_TP_LINE,OBJPROP_HIDDEN,true);
ObjectSetInteger(0,N_TP_LINE,OBJPROP_ZORDER,5);
if(!Label(N_TP_PRICE,"",0,0,FS(8),C'46,196,145',"Segoe UI Semibold",6))
{ Del(N_TP_LINE); return; }
ObjectSetInteger(0,N_TP_PRICE,OBJPROP_ANCHOR,ANCHOR_RIGHT_LOWER);
Visible(N_TP_PRICE,false);
g_tp_initialized=true;
}
if(ObjectGetDouble(0,N_TP_LINE,OBJPROP_PRICE)!=target)
ObjectSetDouble(0,N_TP_LINE,OBJPROP_PRICE,target);
g_tp_last_set=target;
g_tp_price=target;
if(seed_price) TPControls();
double profit=0.0;
bool valid=true;
for(int i=0;i<g_steps;i++)
{
string line=i==0 ? N_ENTRY_LINE : N_STAGE_LINE[i-1];
double loss,part=0.0;
PositionVolumeText(loss,line);
if(loss<0.0) { valid=false; break; }
double open=i==0 ? entry : ObjectGetDouble(0,line,OBJPROP_PRICE);
if(g_calc_volume>0.0 && !OrderCalcProfit(buy ? ORDER_TYPE_BUY : ORDER_TYPE_SELL,
_Symbol,g_calc_volume,open,target,part))
{ valid=false; break; }
profit+=part;
}
string money="Profit: --";
if(valid) money=MoneyUSD(profit,g_steps>1 ? "Total profit" : "Profit");
double rr=(target-entry)/(entry-stop);
string prefix="TP | "+money+" | R:R "+DoubleToString(rr,2)+" |";
UpdatePriceLabel(N_TP_LINE,N_TP_PRICE,prefix,false,g_tp_price,g_tp_caption,g_tp_x,g_tp_y,g_tp_visible);
}
void CommitTP()
{
string value=ObjectGetString(0,N_TP_EDIT,OBJPROP_TEXT);
StringTrimLeft(value); StringTrimRight(value);
bool digit=false,decimal=false,valid=true;
for(int i=0;i<StringLen(value);i++)
{
ushort c=StringGetCharacter(value,i);
if(c>='0' && c<='9') digit=true;
else if(c=='.' && !decimal) decimal=true;
else { valid=false; break; }
}
double number=StringToDouble(value);
if(valid && digit && MathIsValidNumber(number) && number>0.0)
{
if(g_tp_rr) g_tp_reward=number;
else g_tp_price=StopPrice(number);
}
TPControls();
RefreshTP();
SaveState();
ChartRedraw();
}
void UpdateEntryLabel()
{
if(g_entry_initialized)
{
color line_color;
double estimated_loss;
string direction=EntryDirection(line_color);
if((color)ObjectGetInteger(0,N_ENTRY_LINE,OBJPROP_COLOR)!=line_color)
{
ObjectSetInteger(0,N_ENTRY_LINE,OBJPROP_COLOR,line_color);
ObjectSetInteger(0,N_ENTRY_PRICE,OBJPROP_COLOR,line_color);
}
UpdatePriceLabel(N_ENTRY_LINE,N_ENTRY_PRICE,
(g_steps>1 ? "#1 " : "")+direction+" | "+PositionVolumeText(estimated_loss)+" |",true,
g_entry_price,g_entry_caption,g_entry_label_x,
g_entry_label_y,g_entry_label_visible);
RefreshStages();
UpdateStopLabel();
RefreshTP();
}
}
void RefreshEntry()
{
if(g_lines_cleared) return;
bool market=(g_entry_mode==ENTRY_MARKET);
double current=CurrentChartPrice();
if(!g_entry_initialized)
{
if(current<=0.0 && (market || !g_restore_entry)) return;
double price=current;
if(!market)
{
if(g_restore_entry) price=g_saved_entry;
else
{
if(ObjectFind(0,N_STOP_LINE)<0) return;
price=StopPrice((current+ObjectGetDouble(0,N_STOP_LINE,OBJPROP_PRICE))*0.5);
}
}
if(!ObjectCreate(0,N_ENTRY_LINE,OBJ_HLINE,0,0,price)) return;
ObjectSetInteger(0,N_ENTRY_LINE,OBJPROP_COLOR,C_ACCENT);
ObjectSetInteger(0,N_ENTRY_LINE,OBJPROP_STYLE,STYLE_SOLID);
ObjectSetInteger(0,N_ENTRY_LINE,OBJPROP_WIDTH,1);
ObjectSetInteger(0,N_ENTRY_LINE,OBJPROP_BACK,true);
ObjectSetInteger(0,N_ENTRY_LINE,OBJPROP_SELECTABLE,!market);
ObjectSetInteger(0,N_ENTRY_LINE,OBJPROP_SELECTED,!market);
ObjectSetInteger(0,N_ENTRY_LINE,OBJPROP_HIDDEN,true);
ObjectSetInteger(0,N_ENTRY_LINE,OBJPROP_ZORDER,5);
ObjectSetString(0,N_ENTRY_LINE,OBJPROP_TOOLTIP,
market ? "Market entry | follows current price" : "Pending entry | Limit / Stop | drag to move");
if(!Label(N_ENTRY_PRICE,"",0,0,FS(8),C_ACCENT,"Segoe UI Semibold",6))
{
Del(N_ENTRY_LINE);
return;
}
ObjectSetInteger(0,N_ENTRY_PRICE,OBJPROP_ANCHOR,ANCHOR_RIGHT_UPPER);
Visible(N_ENTRY_PRICE,false);
g_entry_initialized=true;
}
// Limit placement happens only on mode entry; ticks never move a manual limit.
if(market && current>0.0 && ObjectFind(0,N_ENTRY_LINE)>=0 &&
ObjectGetDouble(0,N_ENTRY_LINE,OBJPROP_PRICE)!=current)
ObjectSetDouble(0,N_ENTRY_LINE,OBJPROP_PRICE,current);
UpdateEntryLabel();
}
void UpdateEntryModeStyle()
{
bool market=(g_entry_mode==ENTRY_MARKET);
RectStyle(N_MARKET_BG,market ? C_ACCENT : C_BTN,market ? C_ACCENT : C_FRAME);
RectStyle(N_LIMIT_BG,market ? C_BTN : C_ACCENT,market ? C_FRAME : C_ACCENT);
ObjectSetInteger(0,N_MARKET_TX,OBJPROP_COLOR,market ? C_TEXT : C_MUTED);
ObjectSetInteger(0,N_LIMIT_TX,OBJPROP_COLOR,market ? C_MUTED : C_TEXT);
SetTextIfChanged(N_MARKET_TX,"Market");
SetTextIfChanged(N_LIMIT_TX,"Limit");
}
void SetEntryMode(EEntryMode mode)
{
if(mode==g_entry_mode) return;
g_entry_mode=mode;
g_restore_entry=false;
Del(N_ENTRY_LINE);
Del(N_ENTRY_PRICE);
g_entry_initialized=false;
g_entry_caption="";
g_entry_label_x=-1;
g_entry_label_y=-1;
g_entry_label_visible=false;
UpdateEntryModeStyle();
RefreshEntry();
ChartRedraw();
}
bool CreateStopLine()
{
if(g_lines_cleared) return false;
double price=g_saved_stop;
if(!g_restore_stop)
{
double low=ChartGetDouble(0,CHART_PRICE_MIN,0);
double high=ChartGetDouble(0,CHART_PRICE_MAX,0);
MqlTick tick;
if(high<=low || !SymbolInfoTick(_Symbol,tick)) return false;
double current=tick.bid;
if(current<=0.0) current=tick.last;
if(current<=0.0) current=tick.ask;
if(current<=0.0) return false;
// In the bottom 20% of the visible range, place the stop above price.
double edge=(current-low<=(high-low)*0.20) ? high : low;
price=StopPrice((current+edge)*0.5);
}
if(!ObjectCreate(0,N_STOP_LINE,OBJ_HLINE,0,0,price)) return false;
ObjectSetInteger(0,N_STOP_LINE,OBJPROP_COLOR,C'232,103,113');
ObjectSetInteger(0,N_STOP_LINE,OBJPROP_STYLE,STYLE_DASH);
ObjectSetInteger(0,N_STOP_LINE,OBJPROP_WIDTH,1);
ObjectSetInteger(0,N_STOP_LINE,OBJPROP_BACK,true);
ObjectSetInteger(0,N_STOP_LINE,OBJPROP_SELECTABLE,true);
ObjectSetInteger(0,N_STOP_LINE,OBJPROP_SELECTED,true);
ObjectSetInteger(0,N_STOP_LINE,OBJPROP_HIDDEN,true);
ObjectSetInteger(0,N_STOP_LINE,OBJPROP_ZORDER,5);
ObjectSetString(0,N_STOP_LINE,OBJPROP_TOOLTIP,"Stop loss | drag to move");
if(!Label(N_STOP_PRICE,"",0,0,FS(8),C'232,103,113',"Segoe UI Semibold",6))
{
Del(N_STOP_LINE);
return false;
}
ObjectSetInteger(0,N_STOP_PRICE,OBJPROP_ANCHOR,ANCHOR_RIGHT_LOWER);
Visible(N_STOP_PRICE,false);
g_stop_initialized=true;
UpdateStopLabel();
ChartRedraw();
return true;
}
//====================================================================
// Size / layout
//====================================================================
void UpdateRiskUnit()
{
bool dollar=(g_risk_unit==RISK_DOLLAR);
RectStyle(N_DOLLAR_BG,dollar ? C_ACCENT : C_BTN,dollar ? C_ACCENT : C_FRAME);
bool percent=(g_risk_unit==RISK_PERCENT);
bool lot=(g_risk_unit==RISK_LOT);
RectStyle(N_PERCENT_BG,percent ? C_ACCENT : C_BTN,percent ? C_ACCENT : C_FRAME);
RectStyle(N_LOT_BG,lot ? C_ACCENT : C_BTN,lot ? C_ACCENT : C_FRAME);
ObjectSetInteger(0,N_DOLLAR_TX,OBJPROP_COLOR,dollar ? C_TEXT : C_MUTED);
ObjectSetInteger(0,N_PERCENT_TX,OBJPROP_COLOR,percent ? C_TEXT : C_MUTED);
ObjectSetInteger(0,N_LOT_TX,OBJPROP_COLOR,lot ? C_TEXT : C_MUTED);
ChartRedraw();
}
void CommitRiskValue()
{
string value=ObjectGetString(0,N_RISK_EDIT,OBJPROP_TEXT);
StringTrimLeft(value);
StringTrimRight(value);
bool digit=false,decimal=false,valid=true;
for(int i=0;i<StringLen(value);i++)
{
ushort c=StringGetCharacter(value,i);
if(c>='0' && c<='9') digit=true;
else if(c=='.' && !decimal) decimal=true;
else { valid=false; break; }
}
double number=StringToDouble(value);
if(valid && digit && MathIsValidNumber(number) && number>=0.0)
{
// Preserve the user's numeric text; broker volume limits are applied by PositionVolumeText.
g_risk_value=number;
g_risk_text=value;
if(g_risk_unit==RISK_DOLLAR) g_dollar_text=value;
else if(g_risk_unit==RISK_PERCENT) g_percent_text=value;
else g_lot_text=value;
}
ObjectSetString(0,N_RISK_EDIT,OBJPROP_TEXT,g_risk_text);
SaveState();
ChartRedraw();
}
int PanelHeight()
{
if(g_minimized)
return S(HEADER_H);
return S(HEADER_H + BODY_H);
}
void Layout(bool redraw=true)
{
int w=S(BASE_W);
int h=PanelHeight();
XYWH(N_SHADOW,g_x+S(3),g_y+S(4),w,h);
XYWH(N_FRAME,g_x,g_y,w,h);
XYWH(N_BODY,g_x+S(1),g_y+S(1),w-S(2),h-S(2));
XYWH(N_HEADER,g_x+S(1),g_y+S(1),w-S(2),S(HEADER_H)-S(1));
XYWH(N_TOPLINE,g_x+S(1),g_y+S(1),w-S(2),S(2));
XY(N_TITLE,g_x+S(6),g_y+S(5));
XY(N_SUBTITLE,g_x+S(176),g_y+S(6));
XYWH(N_MIN_BG,g_x+w-S(46),g_y+S(3),S(20),S(20));
CenterText(N_MIN_TX,g_x+w-S(46),g_y+S(3),S(20),S(20));
XYWH(N_CLOSE_BG,g_x+w-S(24),g_y+S(3),S(20),S(20));
CenterText(N_CLOSE_TX,g_x+w-S(24),g_y+S(3),S(20),S(20));
ObjectSetString(0,N_MIN_TX,OBJPROP_TEXT,g_minimized ? "+" : "–");
bool show=!g_minimized;
Visible(N_LEFT_BG,show);
Visible(N_CLEAR_BG,show); Visible(N_CLEAR_TX,show);
Visible(N_LEFT_TX,show);
Visible(N_RIGHT_BG,show);
Visible(N_RIGHT_TX,show);
Visible(N_RISK_EDIT,show);
Visible(N_DOLLAR_BG,show);
Visible(N_DOLLAR_TX,show);
Visible(N_PERCENT_BG,show);
Visible(N_PERCENT_TX,show);
Visible(N_LOT_BG,show); Visible(N_LOT_TX,show);
Visible(N_MARKET_BG,show);
Visible(N_MARKET_TX,show);
Visible(N_LIMIT_BG,show);
Visible(N_LIMIT_TX,show);
Visible(N_TP_TITLE,show); Visible(N_TP_EDIT,show);
Visible(N_TP_RR_BG,show); Visible(N_TP_RR_TX,show);
Visible(N_TP_VALUE_BG,show); Visible(N_TP_VALUE_TX,show);
Visible(N_STEPS_TITLE,show);
Visible(N_DAY_TITLE,show); Visible(N_DAY_VALUE,show);
for(int i=0;i<3;i++) { Visible(N_STEPS_BG[i],show); Visible(N_STEPS_TX[i],show); }
if(show)
{
int left =g_x+S(OUTER_PAD);
int right=g_x+w-S(OUTER_PAD);
int y=g_y+S(HEADER_H+BODY_H-OUTER_PAD-BUTTON_H);
int risk_y=g_y+S(HEADER_H+8);
XYWH(N_RISK_EDIT,left,risk_y,S(126),S(28));
XYWH(N_DOLLAR_BG,left+S(134),risk_y,S(36),S(28));
CenterText(N_DOLLAR_TX,left+S(134),risk_y,S(36),S(28));
XYWH(N_PERCENT_BG,left+S(174),risk_y,S(36),S(28));
CenterText(N_PERCENT_TX,left+S(174),risk_y,S(36),S(28));
XYWH(N_LOT_BG,left+S(214),risk_y,S(44),S(28));
CenterText(N_LOT_TX,left+S(214),risk_y,S(44),S(28));
int mode_y=risk_y+S(34);
XYWH(N_MARKET_BG,left,mode_y,S(127),S(26));
CenterText(N_MARKET_TX,left,mode_y,S(127),S(26));
XYWH(N_LIMIT_BG,left+S(131),mode_y,S(127),S(26));
CenterText(N_LIMIT_TX,left+S(131),mode_y,S(127),S(26));
int tp_y=mode_y+S(34);
XY(N_TP_TITLE,left,tp_y+S(6));
XYWH(N_TP_EDIT,left+S(24),tp_y,S(120),S(28));
XYWH(N_TP_RR_BG,left+S(150),tp_y,S(48),S(28));
CenterText(N_TP_RR_TX,left+S(150),tp_y,S(48),S(28));
XYWH(N_TP_VALUE_BG,left+S(202),tp_y,S(56),S(28));
CenterText(N_TP_VALUE_TX,left+S(202),tp_y,S(56),S(28));
int steps_y=tp_y+S(34);
XY(N_DAY_TITLE,left,steps_y+S(34));
ObjectSetInteger(0,N_DAY_VALUE,OBJPROP_ANCHOR,ANCHOR_RIGHT_UPPER);
XY(N_DAY_VALUE,right,steps_y+S(34));
XY(N_STEPS_TITLE,left,steps_y+S(5));
for(int i=0;i<3;i++)
{
XYWH(N_STEPS_BG[i],left+S(102+i*52),steps_y,S(50),S(26));
CenterText(N_STEPS_TX[i],left+S(102+i*52),steps_y,S(50),S(26));
}
XYWH(N_LEFT_BG,left,y,S(72),S(BUTTON_H));
CenterText(N_LEFT_TX,left,y,S(72),S(BUTTON_H));
XYWH(N_CLEAR_BG,left+S(78),y,S(70),S(BUTTON_H));
CenterText(N_CLEAR_TX,left+S(78),y,S(70),S(BUTTON_H));
XYWH(N_RIGHT_BG,right-S(104),y,S(104),S(BUTTON_H));
CenterText(N_RIGHT_TX,right-S(104),y,S(104),S(BUTTON_H));
}
if(redraw)
ChartRedraw();
}
//====================================================================
// Build / delete
//====================================================================
void DeleteAll()
{
string names[]=
{
N_SHADOW,N_FRAME,N_BODY,N_HEADER,N_TOPLINE,
N_TITLE,N_SUBTITLE,
N_MIN_BG,N_MIN_TX,N_CLOSE_BG,N_CLOSE_TX,
N_LEFT_BG,N_LEFT_TX,N_RIGHT_BG,N_RIGHT_TX,N_CLEAR_BG,N_CLEAR_TX,
N_RISK_EDIT,N_DOLLAR_BG,N_DOLLAR_TX,N_PERCENT_BG,N_PERCENT_TX,N_LOT_BG,N_LOT_TX,
N_STOP_LINE,N_STOP_PRICE,N_ENTRY_LINE,N_ENTRY_PRICE,
N_MARKET_BG,N_MARKET_TX,N_LIMIT_BG,N_LIMIT_TX,
N_TP_LINE,N_TP_PRICE,N_TP_TITLE,N_TP_EDIT,N_TP_RR_BG,N_TP_RR_TX,N_TP_VALUE_BG,N_TP_VALUE_TX
};
for(int i=0;i<ArraySize(names);i++)
Del(names[i]);
Del(N_STEPS_TITLE);
Del(N_DAY_TITLE); Del(N_DAY_VALUE);
for(int i=0;i<3;i++) { Del(N_STEPS_BG[i]); Del(N_STEPS_TX[i]); }
for(int i=0;i<2;i++)
{
Del(N_STAGE_LINE[i]); Del(N_STAGE_LABEL[i]);
g_stage_caption[i]=""; g_stage_x[i]=-1; g_stage_y[i]=-1; g_stage_visible[i]=false;
}
g_stop_initialized=false;
g_stop_caption="";
g_stop_label_x=-1;
g_stop_label_y=-1;
g_stop_label_visible=false;
g_entry_initialized=false;
g_entry_caption="";
g_entry_label_x=-1;
g_entry_label_y=-1;
g_entry_label_visible=false;
g_tp_initialized=false; g_tp_caption="";
g_tp_x=-1; g_tp_y=-1; g_tp_visible=false;
ChartRedraw();
}
bool Build()
{
DeleteAll();
int w=S(BASE_W);
int h=PanelHeight();
if(!Rect(N_SHADOW,g_x+S(3),g_y+S(4),w,h,C_SHADOW,C_SHADOW,0)) return false;
if(!Rect(N_FRAME,g_x,g_y,w,h,C_FRAME,C_FRAME,1)) return false;
if(!Rect(N_BODY,g_x+S(1),g_y+S(1),w-S(2),h-S(2),C_BODY,C_BODY,2)) return false;
if(!Rect(N_HEADER,g_x,g_y,w,S(HEADER_H),C_HEADER,C_HEADER,10)) return false;
if(!Rect(N_TOPLINE,g_x,g_y,w,S(2),C_HEADER_TOP,C_HEADER_TOP,11)) return false;
if(!Label(N_TITLE,"Risk Management",g_x,g_y,FS(8),C_TEXT,"Segoe UI Semibold",20)) return false;
if(!Label(N_SUBTITLE,"| WINDOW",g_x,g_y,FS(5),C_MUTED,"Segoe UI",20)) return false;
if(!Rect(N_MIN_BG,g_x,g_y,S(14),S(14),C_BTN,C_BTN,30)) return false;
if(!Label(N_MIN_TX,"–",g_x,g_y,FS(8),C_TEXT2,"Segoe UI Semibold",31)) return false;
if(!Rect(N_CLOSE_BG,g_x,g_y,S(14),S(14),C_CLOSE,C_CLOSE,30)) return false;
if(!Label(N_CLOSE_TX,"×",g_x,g_y,FS(8),C_TEXT,"Segoe UI Semibold",31)) return false;
if(!ObjectCreate(0,N_RISK_EDIT,OBJ_EDIT,0,0,0)) return false;
ObjectSetInteger(0,N_RISK_EDIT,OBJPROP_CORNER,CORNER_LEFT_UPPER);
ObjectSetInteger(0,N_RISK_EDIT,OBJPROP_BGCOLOR,C_BODY);
ObjectSetInteger(0,N_RISK_EDIT,OBJPROP_BORDER_COLOR,C_FRAME);
ObjectSetInteger(0,N_RISK_EDIT,OBJPROP_COLOR,C_TEXT);
ObjectSetInteger(0,N_RISK_EDIT,OBJPROP_FONTSIZE,FS(9));
ObjectSetInteger(0,N_RISK_EDIT,OBJPROP_ALIGN,ALIGN_CENTER);
ObjectSetInteger(0,N_RISK_EDIT,OBJPROP_READONLY,false);
ObjectSetInteger(0,N_RISK_EDIT,OBJPROP_SELECTABLE,false);
ObjectSetInteger(0,N_RISK_EDIT,OBJPROP_HIDDEN,true);
ObjectSetInteger(0,N_RISK_EDIT,OBJPROP_ZORDER,30);
ObjectSetString(0,N_RISK_EDIT,OBJPROP_FONT,"Segoe UI");
ObjectSetString(0,N_RISK_EDIT,OBJPROP_TEXT,g_risk_text);
ObjectSetString(0,N_RISK_EDIT,OBJPROP_TOOLTIP,"Risk amount (e.g. 10 or 2.5)");
if(!Rect(N_DOLLAR_BG,g_x,g_y,S(36),S(28),C_BTN,C_FRAME,30)) return false;
if(!Label(N_DOLLAR_TX,"$",g_x,g_y,FS(10),C_TEXT,"Segoe UI Semibold",31)) return false;
if(!Rect(N_PERCENT_BG,g_x,g_y,S(36),S(28),C_BTN,C_FRAME,30)) return false;
if(!Label(N_PERCENT_TX,"%",g_x,g_y,FS(10),C_TEXT,"Segoe UI Semibold",31)) return false;
if(!Rect(N_LOT_BG,g_x,g_y,S(44),S(28),C_BTN,C_FRAME,30)) return false;
if(!Label(N_LOT_TX,"Lot",g_x,g_y,FS(8),C_TEXT,"Segoe UI Semibold",31)) return false;
UpdateRiskUnit();
if(!Rect(N_MARKET_BG,g_x,g_y,S(127),S(26),C_BTN,C_FRAME,30)) return false;
if(!Label(N_MARKET_TX,"Market",g_x,g_y,FS(8),C_TEXT,"Segoe UI Semibold",31)) return false;
if(!Rect(N_LIMIT_BG,g_x,g_y,S(127),S(26),C_BTN,C_FRAME,30)) return false;
if(!Label(N_LIMIT_TX,"Limit",g_x,g_y,FS(8),C_TEXT,"Segoe UI Semibold",31)) return false;
UpdateEntryModeStyle();
// Bottom buttons retained from the original panel.
if(!Label(N_TP_TITLE,"TP",g_x,g_y,FS(8),C_MUTED)) return false;
if(!ObjectCreate(0,N_TP_EDIT,OBJ_EDIT,0,0,0)) return false;
ObjectSetInteger(0,N_TP_EDIT,OBJPROP_CORNER,CORNER_LEFT_UPPER);
ObjectSetInteger(0,N_TP_EDIT,OBJPROP_BGCOLOR,C_BODY);
ObjectSetInteger(0,N_TP_EDIT,OBJPROP_BORDER_COLOR,C_FRAME);
ObjectSetInteger(0,N_TP_EDIT,OBJPROP_COLOR,C_TEXT);
ObjectSetInteger(0,N_TP_EDIT,OBJPROP_FONTSIZE,FS(9));
ObjectSetInteger(0,N_TP_EDIT,OBJPROP_ALIGN,ALIGN_CENTER);
ObjectSetInteger(0,N_TP_EDIT,OBJPROP_READONLY,false);
ObjectSetInteger(0,N_TP_EDIT,OBJPROP_SELECTABLE,false);
ObjectSetInteger(0,N_TP_EDIT,OBJPROP_HIDDEN,true);
ObjectSetInteger(0,N_TP_EDIT,OBJPROP_ZORDER,30);
ObjectSetString(0,N_TP_EDIT,OBJPROP_FONT,"Segoe UI");
if(!Rect(N_TP_RR_BG,g_x,g_y,S(48),S(28),C_BTN,C_FRAME,30)) return false;
if(!Label(N_TP_RR_TX,"R:R",g_x,g_y,FS(8),C_TEXT,"Segoe UI Semibold",31)) return false;
if(!Rect(N_TP_VALUE_BG,g_x,g_y,S(56),S(28),C_BTN,C_FRAME,30)) return false;
if(!Label(N_TP_VALUE_TX,"Price",g_x,g_y,FS(8),C_TEXT,"Segoe UI Semibold",31)) return false;
TPControls();
if(!Label(N_STEPS_TITLE,"Entries",g_x,g_y,FS(8),C_MUTED)) return false;
for(int i=0;i<3;i++)
{
if(!Rect(N_STEPS_BG[i],g_x,g_y,S(50),S(26),C_BTN,C_FRAME,30)) return false;
if(!Label(N_STEPS_TX[i],IntegerToString(i+1),g_x,g_y,FS(8),C_TEXT,"Segoe UI Semibold",31)) return false;
}
StepControls();
if(!Label(N_DAY_TITLE,"Today P/L",g_x,g_y,FS(7),C_MUTED)) return false;
if(!Label(N_DAY_VALUE,"-- | --%",g_x,g_y,FS(7),C_MUTED,"Segoe UI Semibold")) return false;
g_day_refresh=0;
if(!Rect(N_LEFT_BG,g_x,g_y,S(72),S(BUTTON_H),C_BTN,C_FRAME,20)) return false;
if(!Label(N_LEFT_TX,"Reset",g_x,g_y,FS(7),C_TEXT2,"Segoe UI",21)) return false;
if(!Rect(N_CLEAR_BG,g_x,g_y,S(70),S(BUTTON_H),C_BTN,C_FRAME,20)) return false;
if(!Label(N_CLEAR_TX,"Clear",g_x,g_y,FS(7),C_TEXT2,"Segoe UI",21)) return false;
if(!Rect(N_RIGHT_BG,g_x,g_y,S(104),S(BUTTON_H),C_ACCENT,C_ACCENT,20)) return false;
if(!Label(N_RIGHT_TX,"EXECUTE",g_x,g_y,FS(7),C_TEXT,"Segoe UI Semibold",21)) return false;
Layout();
return true;
}
//====================================================================
// Hit testing
//====================================================================
bool OverMin(int mx,int my)
{
return HitObject(N_MIN_BG,mx,my);
}
bool OverClose(int mx,int my)
{
return HitObject(N_CLOSE_BG,mx,my);
}
bool OverHeader(int mx,int my)
{
if(!Hit(mx,my,g_x,g_y,S(BASE_W),S(HEADER_H)))
return false;
if(OverMin(mx,my) || OverClose(mx,my))
return false;
return true;
}
bool OverLeft(int mx,int my)
{
return !g_minimized && HitObject(N_LEFT_BG,mx,my);
}
bool OverRight(int mx,int my)
{
return !g_minimized && HitObject(N_RIGHT_BG,mx,my);
}
//====================================================================
// Dragging
//====================================================================
void KeepInside()
{
long cw=0,ch=0;
ChartGetInteger(0,CHART_WIDTH_IN_PIXELS,0,cw);
ChartGetInteger(0,CHART_HEIGHT_IN_PIXELS,0,ch);
int w=S(BASE_W);
int visible_h=PanelHeight();
if(g_x<0) g_x=0;
if(g_y<0) g_y=0;
if(g_x+w>(int)cw)
g_x=(int)MathMax(0,cw-w);
if(g_y+visible_h>(int)ch)
g_y=(int)MathMax(0,ch-visible_h);
}
void BeginDrag(int mx,int my)
{
g_dragging=true;
g_drag_dx=mx-g_x;
g_drag_dy=my-g_y;
long scroll=1;
ChartGetInteger(0,CHART_MOUSE_SCROLL,0,scroll);
g_chart_scroll_before=(scroll!=0);
ChartSetInteger(0,CHART_MOUSE_SCROLL,false);
}
void EndDrag()
{
if(!g_dragging)
return;
g_dragging=false;
KeepInside();
Layout();
ChartSetInteger(0,CHART_MOUSE_SCROLL,g_chart_scroll_before);
}
color ExecuteRiskColor(double percent)
{
if(percent<=1.0) return C'164,224,180';
if(percent<3.0) return C'24,142,72';
double t=MathMin(1.0,(percent-3.0)/7.0);
int red=(int)MathRound(24+(230-24)*t);
int green=(int)MathRound(142*(1.0-t));
int blue=(int)MathRound(72*(1.0-t));
return (color)(red | (green<<8) | (blue<<16));
}
void UpdateExecuteRisk()
{
if(ObjectFind(0,N_RIGHT_BG)<0) return;
double balance=AccountInfoDouble(ACCOUNT_BALANCE);
double total=0.0;
bool valid=(!g_lines_cleared && balance>0.0);
for(int i=0;i<g_steps && valid;i++)
{
double loss;
PositionVolumeText(loss,i==0 ? N_ENTRY_LINE : N_STAGE_LINE[i-1]);
if(loss<0.0 || !MathIsValidNumber(loss)) valid=false;
else total+=loss;
}
double percent=valid ? total/balance*100.0 : 0.0;
color bg=valid ? ExecuteRiskColor(percent) : C_BTN;
color fg=valid && percent<=1.0 ? C'16,55,29' : C_TEXT;
// Preserve the risk hue while the button is held down.
if(g_pressed==PRESS_RIGHT)
{
uint rgb=(uint)bg;
bg=(color)(((rgb&255)*4/5) | ((((rgb>>8)&255)*4/5)<<8) | ((((rgb>>16)&255)*4/5)<<16));
}
string tip=valid ? "Total risk: "+DoubleToString(percent,2)+"% of balance | "+
DoubleToString(total,2)+" "+AccountInfoString(ACCOUNT_CURRENCY)+
" | "+IntegerToString(g_steps)+" entries" : "Risk unavailable: check entry, stop and balance";
if(valid && percent>=10.0) tip+=" | HIGH RISK";
bool changed=((color)ObjectGetInteger(0,N_RIGHT_BG,OBJPROP_BGCOLOR)!=bg ||
(color)ObjectGetInteger(0,N_RIGHT_TX,OBJPROP_COLOR)!=fg ||
ObjectGetString(0,N_RIGHT_BG,OBJPROP_TOOLTIP)!=tip);
if(!changed) return;
RectStyle(N_RIGHT_BG,bg,bg);
ObjectSetInteger(0,N_RIGHT_TX,OBJPROP_COLOR,fg);
ObjectSetString(0,N_RIGHT_BG,OBJPROP_TOOLTIP,tip);
ObjectSetString(0,N_RIGHT_TX,OBJPROP_TOOLTIP,tip);
ChartRedraw();
}
void ResetPressed()
{
RectStyle(N_MIN_BG,C_BTN,C_BTN);
RectStyle(N_CLOSE_BG,C_CLOSE,C_CLOSE);
RectStyle(N_LEFT_BG,C_BTN,C_FRAME);
UpdateExecuteRisk();
ChartRedraw();
}
//====================================================================
// Lifecycle
//====================================================================
void ExecutionError(string message)
{
Print("RiskManagement: ",message);
Alert("RiskManagement: ",message);
ObjectSetString(0,N_RIGHT_BG,OBJPROP_TOOLTIP,message);
}
bool PrepareRequest(int stage,MqlTradeRequest &request,string &error)
{
ZeroMemory(request);
string line=stage==0 ? N_ENTRY_LINE : N_STAGE_LINE[stage-1];
if(ObjectFind(0,line)<0 || ObjectFind(0,N_STOP_LINE)<0 || ObjectFind(0,N_TP_LINE)<0)
{ error="Missing entry, SL or TP line."; return false; }
MqlTick tick;
if(!SymbolInfoTick(_Symbol,tick) || tick.ask<=0.0 || tick.bid<=0.0)
{ error="No valid quote."; return false; }
double first=ObjectGetDouble(0,N_ENTRY_LINE,OBJPROP_PRICE);
double sl=StopPrice(ObjectGetDouble(0,N_STOP_LINE,OBJPROP_PRICE));
double tp=StopPrice(ObjectGetDouble(0,N_TP_LINE,OBJPROP_PRICE));
bool buy=first>sl;
bool market=(stage==0 && g_entry_mode==ENTRY_MARKET);
double price=market ? (buy ? tick.ask : tick.bid) : StopPrice(ObjectGetDouble(0,line,OBJPROP_PRICE));
double loss;
PositionVolumeText(loss,line);
if(loss<0.0 || g_calc_volume<=0.0)
{ error="Invalid risk/SL or volume below broker minimum."; return false; }
if(price<=0.0 || sl<=0.0 || tp<=0.0 ||
(buy && (sl>=price || tp<=price)) || (!buy && (sl<=price || tp>=price)))
{ error="SL and TP must be on the correct sides of every entry."; return false; }
double gap=SymbolInfoInteger(_Symbol,SYMBOL_TRADE_STOPS_LEVEL)*_Point;
double reference=market ? (buy ? tick.bid : tick.ask) : price;
if((buy && (reference-sl<gap || tp-reference<gap)) ||
(!buy && (sl-reference<gap || reference-tp<gap)))
{ error="SL/TP is too close for the broker's stop distance."; return false; }
request.action=market ? TRADE_ACTION_DEAL : TRADE_ACTION_PENDING;
request.type=buy ? ORDER_TYPE_BUY : ORDER_TYPE_SELL;
if(!market)
{
double quote=buy ? tick.ask : tick.bid;
if(MathAbs(price-quote)<MathMax(gap,_Point*0.5))
{ error="Pending entry is too close to the current quote."; return false; }
request.type=buy ? (price<quote ? ORDER_TYPE_BUY_LIMIT : ORDER_TYPE_BUY_STOP)
: (price>quote ? ORDER_TYPE_SELL_LIMIT : ORDER_TYPE_SELL_STOP);
}
request.symbol=_Symbol;
request.magic=InpMagicNumber;
request.volume=g_calc_volume;
request.price=price;
request.sl=sl; request.tp=tp;
request.deviation=InpDeviationPoints;
request.type_time=ORDER_TIME_GTC;
request.type_filling=ORDER_FILLING_RETURN;
if(market)
{
long flags=SymbolInfoInteger(_Symbol,SYMBOL_FILLING_MODE);
if((flags&SYMBOL_FILLING_FOK)!=0) request.type_filling=ORDER_FILLING_FOK;
else if((flags&SYMBOL_FILLING_IOC)!=0) request.type_filling=ORDER_FILLING_IOC;
else if(SymbolInfoInteger(_Symbol,SYMBOL_TRADE_EXEMODE)==SYMBOL_TRADE_EXECUTION_MARKET)
{ error="No supported market filling policy."; return false; }
}
request.comment="RiskManagement #"+IntegerToString(stage+1);
return true;
}
void ExecutePlan()
{
if(g_executing) return;
if(g_lines_cleared)
{ ExecutionError("Use Reset to create a trading plan first."); return; }
if(!TerminalInfoInteger(TERMINAL_CONNECTED) || !TerminalInfoInteger(TERMINAL_TRADE_ALLOWED) ||
!MQLInfoInteger(MQL_TRADE_ALLOWED) || !AccountInfoInteger(ACCOUNT_TRADE_ALLOWED) ||
!AccountInfoInteger(ACCOUNT_TRADE_EXPERT))
{ ExecutionError("Trading is unavailable. Check connection and Algo Trading permissions."); return; }
// Netting adds to or offsets an existing position, including unrelated trades.
if(AccountInfoInteger(ACCOUNT_MARGIN_MODE)!=ACCOUNT_MARGIN_MODE_RETAIL_HEDGING && PositionSelect(_Symbol))
{ ExecutionError("An existing netting position on this symbol must be managed before starting a new plan."); return; }
CommitRiskValue(); CommitTP(); RefreshEntry();
MqlTradeRequest requests[3];
string error;
double total_margin=0.0;
for(int i=0;i<g_steps;i++)
{
if(!PrepareRequest(i,requests[i],error))
{ ExecutionError("Entry "+IntegerToString(i+1)+": "+error); return; }
MqlTradeCheckResult check={};
if(!OrderCheck(requests[i],check))
{ ExecutionError("Entry "+IntegerToString(i+1)+": "+check.comment+" ("+IntegerToString(check.retcode)+")"); return; }
double margin=0.0;
bool buy=requests[i].type==ORDER_TYPE_BUY || requests[i].type==ORDER_TYPE_BUY_LIMIT || requests[i].type==ORDER_TYPE_BUY_STOP;
if(!OrderCalcMargin(buy ? ORDER_TYPE_BUY : ORDER_TYPE_SELL,_Symbol,requests[i].volume,requests[i].price,margin))
{ ExecutionError("Cannot calculate margin for entry "+IntegerToString(i+1)); return; }
total_margin+=margin;
}
if(total_margin>AccountInfoDouble(ACCOUNT_MARGIN_FREE))
{ ExecutionError("Insufficient free margin for all entries."); return; }
// Guard only the active submission; later clicks may execute the same plan again.
g_executing=true;
int accepted=0;
string details="";
for(int i=0;i<g_steps;i++)
{
MqlTradeCheckResult check={};
if(!OrderCheck(requests[i],check))
{ details="Entry "+IntegerToString(i+1)+" check failed: "+check.comment; break; }
MqlTradeResult result={};
bool sent=OrderSend(requests[i],result);
PrintFormat("Entry %d: sent=%s retcode=%u order=%I64u deal=%I64u volume=%.8f %s",
i+1,sent ? "true" : "false",result.retcode,result.order,result.deal,result.volume,result.comment);
if(sent && (result.retcode==TRADE_RETCODE_DONE || result.retcode==TRADE_RETCODE_PLACED ||
result.retcode==TRADE_RETCODE_DONE_PARTIAL))
{
accepted++;
if(result.retcode==TRADE_RETCODE_DONE_PARTIAL)
{ details="Entry "+IntegerToString(i+1)+" partially filled; remaining entries were not sent."; break; }
}
else
{ details="Entry "+IntegerToString(i+1)+": "+result.comment+" ("+IntegerToString(result.retcode)+"). Check Trade/History before retrying."; break; }
}
g_executing=false;
string summary=IntegerToString(accepted)+"/"+IntegerToString(g_steps)+" entries accepted. "+details;
ObjectSetString(0,N_RIGHT_BG,OBJPROP_TOOLTIP,summary);
if(accepted<g_steps || details!="") ExecutionError(summary);
else
{
Print(summary);
ClearLines();
UpdateExecuteRisk();
}
ChartRedraw();
}
int OnInit()
{
g_scale=InpUIScale;
if(g_scale<0.70) g_scale=0.70;
if(g_scale>1.50) g_scale=1.50;
// Chart-local storage is also symbol-specific; never reuse another symbol's prices.
// Legacy state has no symbol identity and therefore cannot be safely migrated.
N_STATE="RM_STATE_V2_"+_Symbol;
N_EXEC_LOCK="RM_EXEC_"+IntegerToString(AccountInfoInteger(ACCOUNT_LOGIN))+"_"+_Symbol;
// Remove the persistent submission guard left by earlier versions.
Del(N_EXEC_LOCK);
SetDefaults();
LoadState();
g_risk_text=g_risk_unit==RISK_DOLLAR ? g_dollar_text : (g_risk_unit==RISK_PERCENT ? g_percent_text : g_lot_text);
g_risk_value=StringToDouble(g_risk_text);
g_prev_left=false;
g_dragging=false;
g_pressed=PRESS_NONE;
ChartSetInteger(0,CHART_EVENT_MOUSE_MOVE,true);
if(!Build())
return INIT_FAILED;
// Retry from the timer if chart geometry or quotes are not ready yet.
CreateStopLine();
RefreshEntry();
if(!EventSetMillisecondTimer(50)) return INIT_FAILED;
g_state_ready=true;
SaveState();
return INIT_SUCCEEDED;
}
void OnDeinit(const int reason)
{
EventKillTimer();
if(g_state_ready && ObjectFind(0,N_RISK_EDIT)>=0) CommitRiskValue();
if(g_state_ready && ObjectFind(0,N_TP_EDIT)>=0) CommitTP();
SaveState();
g_state_ready=false;
if(g_dragging)
ChartSetInteger(0,CHART_MOUSE_SCROLL,g_chart_scroll_before);
DeleteAll();
}
void OnTick()
{
RefreshEntry();
}
void UpdateDailyPL()
{
ulong now_ms=GetTickCount64();
if(g_day_refresh!=0 && now_ms-g_day_refresh<5000) return;
g_day_refresh=now_ms;
datetime now=TimeTradeServer();
if(now<=0) now=TimeCurrent();
MqlDateTime day;
if(now<=0 || !TimeToStruct(now,day)) return;
day.hour=0; day.min=0; day.sec=0;
datetime start=StructToTime(day);
if(!HistorySelect(start,now))
{
SetTextIfChanged(N_DAY_VALUE,"-- | --%");
ObjectSetInteger(0,N_DAY_VALUE,OBJPROP_COLOR,C_MUTED);
return;
}
double net=0.0,balance_change=0.0;
for(int i=0;i<HistoryDealsTotal();i++)
{
ulong ticket=HistoryDealGetTicket(i);
if(ticket==0) continue;
ENUM_DEAL_TYPE type=(ENUM_DEAL_TYPE)HistoryDealGetInteger(ticket,DEAL_TYPE);
double amount=HistoryDealGetDouble(ticket,DEAL_PROFIT)+HistoryDealGetDouble(ticket,DEAL_COMMISSION)+
HistoryDealGetDouble(ticket,DEAL_SWAP)+HistoryDealGetDouble(ticket,DEAL_FEE);
// Deposits/withdrawals affect the opening-balance reconstruction, not trading P/L.
if(type!=DEAL_TYPE_CREDIT) balance_change+=amount;
bool trade=(type==DEAL_TYPE_BUY || type==DEAL_TYPE_SELL);
bool commission=(type==DEAL_TYPE_COMMISSION || type==DEAL_TYPE_COMMISSION_DAILY ||
type==DEAL_TYPE_COMMISSION_MONTHLY || type==DEAL_TYPE_COMMISSION_AGENT_DAILY ||
type==DEAL_TYPE_COMMISSION_AGENT_MONTHLY);
if(trade || commission) net+=amount;
}
double opening=AccountInfoDouble(ACCOUNT_BALANCE)-balance_change;
string percent=opening>0.0 ? (net>0.0 ? "+" : "")+DoubleToString(net/opening*100.0,2)+"%" : "--%";
string money=MoneyUSD(net,"");
if(StringFind(money,": $")==0) money=StringSubstr(money,2);
else money="USD --";
if(net>0.0 && StringFind(money,"$")==0) money="+"+money;
string text=money+" | "+percent;
SetTextIfChanged(N_DAY_VALUE,text);
ObjectSetInteger(0,N_DAY_VALUE,OBJPROP_COLOR,net>0.005 ? C'46,196,145' : (net< -0.005 ? C'232,103,113' : C_MUTED));
ObjectSetString(0,N_DAY_VALUE,OBJPROP_TOOLTIP,
"All account trades | Broker day "+TimeToString(start,TIME_DATE)+
" | Realized P/L + commissions, swap and fees booked today (including entry fees). Floating P/L excluded."+
" | Opening balance: "+DoubleToString(opening,2)+" "+AccountInfoString(ACCOUNT_CURRENCY));
ChartRedraw();
}
void OnTradeTransaction(const MqlTradeTransaction &transaction,const MqlTradeRequest &request,const MqlTradeResult &result)
{
g_day_refresh=0;
}
void OnTimer()
{
UpdateDailyPL();
if(!g_stop_initialized) CreateStopLine();
else UpdateStopLabel();
RefreshEntry();
SaveState();
UpdateExecuteRisk();
}
//====================================================================
// Events
//====================================================================
void OnChartEvent(const int id,
const long &lparam,
const double &dparam,
const string &sparam)
{
if(id==CHARTEVENT_OBJECT_CLICK && !g_minimized && (sparam==N_CLEAR_BG || sparam==N_CLEAR_TX))
{ ClearLines(); return; }
for(int i=0;i<2;i++)
if((id==CHARTEVENT_OBJECT_DRAG || id==CHARTEVENT_OBJECT_CHANGE) && sparam==N_STAGE_LINE[i])
{
ObjectSetDouble(0,N_STAGE_LINE[i],OBJPROP_PRICE,StopPrice(ObjectGetDouble(0,N_STAGE_LINE[i],OBJPROP_PRICE)));
UpdateEntryLabel(); SaveState(); ChartRedraw(); return;
}
if(id==CHARTEVENT_OBJECT_CLICK && !g_minimized)
for(int i=0;i<3;i++)
if(sparam==N_STEPS_BG[i] || sparam==N_STEPS_TX[i])
{
CommitRiskValue(); g_steps=i+1; StepControls();
UpdateEntryLabel(); SaveState(); ChartRedraw(); return;
}
if(id==CHARTEVENT_OBJECT_ENDEDIT && sparam==N_TP_EDIT)
{ CommitTP(); return; }
if((id==CHARTEVENT_OBJECT_DRAG || id==CHARTEVENT_OBJECT_CHANGE) && sparam==N_TP_LINE)
{
g_tp_rr=false;
g_tp_price=StopPrice(ObjectGetDouble(0,N_TP_LINE,OBJPROP_PRICE));
ObjectSetDouble(0,N_TP_LINE,OBJPROP_PRICE,g_tp_price);
g_tp_last_set=g_tp_price;
TPControls(); RefreshTP(); SaveState(); ChartRedraw();
return;
}
if(id==CHARTEVENT_CHART_CHANGE)
{
KeepInside();
Layout();
UpdateStopLabel();
UpdateEntryLabel();
return;
}
if((id==CHARTEVENT_OBJECT_DRAG || id==CHARTEVENT_OBJECT_CHANGE) && sparam==N_ENTRY_LINE)
{
if(g_entry_mode==ENTRY_LIMIT)
ObjectSetDouble(0,N_ENTRY_LINE,OBJPROP_PRICE,
StopPrice(ObjectGetDouble(0,N_ENTRY_LINE,OBJPROP_PRICE)));
RefreshEntry();
ChartRedraw();
return;
}
if((id==CHARTEVENT_OBJECT_DRAG || id==CHARTEVENT_OBJECT_CHANGE) && sparam==N_STOP_LINE)
{
double price=StopPrice(ObjectGetDouble(0,N_STOP_LINE,OBJPROP_PRICE));
ObjectSetDouble(0,N_STOP_LINE,OBJPROP_PRICE,price);
UpdateStopLabel();
UpdateEntryLabel();
ChartRedraw();
return;
}
if(id==CHARTEVENT_OBJECT_ENDEDIT && sparam==N_RISK_EDIT)
{
CommitRiskValue();
return;
}
if(id==CHARTEVENT_OBJECT_CLICK && !g_minimized)
{
if(sparam==N_DOLLAR_BG || sparam==N_DOLLAR_TX)
{
SetRiskUnit(RISK_DOLLAR);
}
else if(sparam==N_PERCENT_BG || sparam==N_PERCENT_TX)
{
SetRiskUnit(RISK_PERCENT);
}
else if(sparam==N_LOT_BG || sparam==N_LOT_TX)
SetRiskUnit(RISK_LOT);
else if(sparam==N_MARKET_BG || sparam==N_MARKET_TX)
SetEntryMode(ENTRY_MARKET);
else if(sparam==N_LIMIT_BG || sparam==N_LIMIT_TX)
SetEntryMode(ENTRY_LIMIT);
else if(sparam==N_TP_RR_BG || sparam==N_TP_RR_TX ||
sparam==N_TP_VALUE_BG || sparam==N_TP_VALUE_TX)
{
CommitTP();
g_tp_rr=(sparam==N_TP_RR_BG || sparam==N_TP_RR_TX);
TPControls(); RefreshTP(); SaveState(); ChartRedraw();
}
return;
}
if(id!=CHARTEVENT_MOUSE_MOVE)
return;
int mx=(int)lparam;
int my=(int)dparam;
// Read the actual line position during native dragging, not only on release.
UpdateStopLabel();
UpdateEntryLabel();
uint state=(uint)StringToInteger(sparam);
bool left=((state&1)==1);
bool down=(left && !g_prev_left);
bool up=(!left && g_prev_left);
if(down)
{
g_pressed=PRESS_NONE;
if(OverMin(mx,my))
{
g_pressed=PRESS_MIN;
RectStyle(N_MIN_BG,C_BTN_PRESS,C_BTN_PRESS);
ChartRedraw();
}
else if(OverClose(mx,my))
{
g_pressed=PRESS_CLOSE;
RectStyle(N_CLOSE_BG,C_CLOSE_PRESS,C_CLOSE_PRESS);
ChartRedraw();
}
else if(OverLeft(mx,my))
{
g_pressed=PRESS_LEFT;
RectStyle(N_LEFT_BG,C_BTN_PRESS,C_FRAME);
ChartRedraw();
}
else if(OverRight(mx,my))
{
g_pressed=PRESS_RIGHT;
UpdateExecuteRisk();
ChartRedraw();
}
else if(OverHeader(mx,my))
{
g_pressed=PRESS_HEADER;
BeginDrag(mx,my);
}
}
if(left && g_dragging && g_pressed==PRESS_HEADER)
{
g_x=mx-g_drag_dx;
g_y=my-g_drag_dy;
KeepInside();
Layout(false);
ChartRedraw();
}
if(up)
{
EPressTarget pressed=g_pressed;
if(g_dragging)
EndDrag();
g_pressed=PRESS_NONE;
ResetPressed();
if(pressed==PRESS_MIN && OverMin(mx,my))
{
g_minimized=!g_minimized;
KeepInside();
Layout();
}
else if(pressed==PRESS_CLOSE && OverClose(mx,my))
{
g_prev_left=left;
ExpertRemove();
return;
}
else if(pressed==PRESS_LEFT && OverLeft(mx,my))
{
ResetAll();
}
else if(pressed==PRESS_RIGHT && OverRight(mx,my))
{
ExecutePlan();
}
}
g_prev_left=left;
}
//+------------------------------------------------------------------+