Market_Replay_Mt5/MarketReplay_v9.mq5
2026-10-27 07:30:23 +00:00
2816 lines
108 KiB
MQL5
//+------------------------------------------------------------------+
//| MarketReplay.mq5 |
//| v9.10 - Tick engine | Pending orders | Speed slider | Trades |
//| Virtual microsecond clock (0.1 ms grid), tick-by-tick SL/TP, |
//| fire-on-press UI, right-click Limit/Stop orders, floating table |
//+------------------------------------------------------------------+
#property copyright "Grok Replay"
#property version "9.10"
#property strict
#property description "Tick-level Market Replay with Limit/Stop orders, speed slider, calendar range picker and Trades window"
//=== ENUM ==========================================================
enum ENUM_TRADE_MODE
{
MODE_SINGLE = 0, // Single Trade | تک‌معامله
MODE_MARTINGALE = 1 // Martingale Basket | سبد مارتینگل
};
//=== INPUTS (Bilingual) ============================================
input group "══════ [1] ACCOUNT & TRADE MODE | حساب و حالت معامله ══════"
input double InpBalance = 500.0; // Starting Balance | موجودی اولیه
input ENUM_TRADE_MODE InpTradeMode = MODE_MARTINGALE; // Trade Mode | حالت معامله
input double InpLot = 0.02; // Initial Lot (editable in panel) | لات اولیه (در پنل قابل تغییر است)
input double InpLotMin = 0.01; // Minimum Lot | حداقل لات
input double InpLotMax = 100.0; // Maximum Lot | حداکثر لات
input double InpLotStep = 0.01; // Lot Step | گام تغییر لات
input group "══════ [2] INSTRUMENT & COSTS | مشخصات نماد و هزینه‌ها ══════"
input double InpPoint = 0.01; // Point Size (Gold = 0.01) | اندازه‌ی یک پوینت (طلا = 0.01)
input double InpContractSize = 100.0; // Contract Size (Gold = 100) | اندازه‌ی قرارداد (طلا = 100)
input int InpSpreadPts = 26; // Fixed Spread in points (0 = none) | اسپرد ثابت به پوینت (صفر = بدون اسپرد)
input group "══════ [3] LEVERAGE & MARGIN | لوریج و مارجین ══════"
input int InpLeverage = 500; // Leverage 1:X (editable in panel) | لوریج ۱:X (در پنل قابل تغییر است)
input double InpStopOutPct = 50.0; // Stop-out Margin Level % | سطح استاپ‌اوت (درصد)
input bool InpAutoReset = true; // Auto-reset Balance on Stop-out | ریست خودکار بالانس هنگام استاپ‌اوت
input group "══════ [4] SINGLE TRADE MODE | حالت تک‌معامله ══════"
input int InpSLpts = 150; // Stop Loss in points | حد ضرر (پوینت)
input int InpTPpts = 300; // Take Profit in points | حد سود (پوینت)
input bool InpTrailing = true; // Enable Trailing Stop | فعال‌سازی تریلینگ استاپ
input int InpTrailStart = 170; // Trailing Start in points of profit | شروع تریلینگ (پوینت سود)
input int InpTrailStep = 50; // Trailing Step in points | فاصله‌ی تریلینگ از قیمت (پوینت)
input group "══════ [5] MARTINGALE BASKET MODE | حالت سبد مارتینگل ══════"
input int InpMartTPpts = 300; // Basket Take Profit in points | حد سود سبد (پوینت از میانگین ورود)
input int InpMartTrailStart = 170; // Basket Trailing Start in points | شروع تریلینگ سبد (پوینت)
input int InpMartTrailStep = 50; // Basket Trailing Step in points | فاصله‌ی تریلینگ سبد (پوینت)
input double InpMartMultiplier = 1.0; // Lot Multiplier after a Loss | ضریب افزایش لات پس از ضرر
input int InpMartMaxLevels = 40; // Maximum Martingale Levels | بیشترین سطح مارتینگل
input group "══════ [6] TICK ENGINE & SPEED | موتور تیک و سرعت ══════"
input ENUM_TIMEFRAMES InpBaseTF = PERIOD_M1; // Base Data Timeframe (finest) | تایم‌فریم پایه‌ی داده (ریزترین)
input int InpMinTicks = 8; // Minimum Ticks per Bar | کمترین تعداد تیک در هر کندل
input int InpMaxTicks = 1200; // Maximum Ticks per Bar | بیشترین تعداد تیک در هر کندل
input double InpPathNoise = 0.5; // Intrabar Path Noise 0..1 | نویز مسیر قیمت داخل کندل (۰ تا ۱)
input int InpUiMs = 40; // Panel Refresh in ms | بازه‌ی به‌روزرسانی پنل (میلی‌ثانیه)
input bool InpFireOnPress = true; // Fire Buttons on Mouse-Down | اجرای دکمه‌ها هنگام فشردن ماوس
input group "══════ [7] CHART LINES | خطوط روی چارت ══════"
input color InpColEntry = clrDodgerBlue; // Entry / Pending Line Colour | رنگ خط ورود و سفارش‌های Pending
input color InpColSL = clrRed; // Stop Loss Line Colour | رنگ خط حد ضرر
input color InpColTP = clrLimeGreen; // Take Profit Line Colour | رنگ خط حد سود
input group "══════ [8] DATA & HISTORY | داده و تاریخچه ══════"
input int InpHistoryBars = 30000; // Base Bars Shown before Start | تعداد کندل پایه پیش از نقطه‌ی شروع
input int InpBarsToSave = 300000; // Max Bars to Save per Timeframe | حداکثر کندل ذخیره برای هر تایم‌فریم
input int InpBarsToLoad = 150000; // Max Bars to Load per Timeframe | حداکثر کندل بارگذاری برای هر تایم‌فریم
input bool InpResaveOnInit = true; // Re-export History on Every Start | ذخیره‌ی دوباره‌ی تاریخچه در هر اجرا
//=== Types =========================================================
struct Pos
{
ulong ticket;
int dir; // +1 BUY, -1 SELL
double lots, open, sl, tp;
datetime time;
bool open_;
int level;
};
struct Ord // pending order
{
ulong ticket;
int type; // 1 BuyLimit, 2 SellLimit, 3 BuyStop, 4 SellStop
double price, lots;
datetime time;
bool live;
};
struct HEv // trade-history mark queued for drawing
{
ulong ticket;
int kind; // 0 open, 1 close
int dir;
datetime t, t0;
double price, p0, profit;
int part; // 0 = normal event, >0 = partial-close sequence number
double lots; // closed volume (partial close marks)
};
//=== Globals =======================================================
string baseSym, customSym, folderName;
string P = "RP_";
long panelChart = 0;
bool ready = false;
ENUM_TIMEFRAMES usedTF = PERIOD_M1;
ENUM_TIMEFRAMES currentTF = PERIOD_M5;
bool playing = false;
double speedMult = 1.0;
double lotSize = 0.10;
bool editing = false;
int leverage = 500, slPts = 0, tpPts = 0;
double resetBal = 10000.0;
bool trailOn = true;
int trStart = 170, trStep = 50;
int dragKind = 0; ulong dragTicket = 0; double dragPrice = 0, dragPrice0 = 0; string dragName = "";
bool modOpen = false; ulong modTicket = 0; int modX = 0, modY = 0;
const int MOD_W = 300, MOD_H = 150;
bool volOpen = false; ulong volTicket = 0; int volX = 0, volY = 0; // Volume (partial close) popup
HEv hev[]; string hDrawn[]; datetime curTime = 0;
int partSeq = 0;
int cur = 0, total = 0, endBar = 0;
double bal = 0, eq = 0;
MqlRates rates[]; // base series (replayed)
MqlRates cser[]; // coarser TF series concatenated (history stitching)
int cOff[8], cCnt[8]; ENUM_TIMEFRAMES cTF[8]; int cK = 0;
int digitsN = 2;
double spreadPx = 0;
// --- tick engine
double barDurUs = 60e6;
double tkOff[];
double tkBid[];
double wkb[];
int tkN = 0, tkIdx = 0;
double barClockUs = 0;
ulong lastUs = 0;
ulong lastUiUs = 0;
double curBid = 0, curAsk = 0;
MqlTick outTk[];
int outN = 0;
ulong rngS = 88172645463325252;
const double BUDGET_US = 8000.0;
// --- UI flags
bool uiForce = false, linesDirty = true, btnDirty = true, needCompact = false, ordCompact = false;
bool mouseDown = false, rightDown = false;
string lastMsg = "";
string pendLog = "";
string pressId = ""; ulong pressMs = 0;
// --- registry of clickable panel buttons
string rId[48]; int rX[48], rY[48], rW[48], rH[48]; int rN = 0;
int panW = 480, panH = 400;
// --- label cache (avoid redundant ObjectSet calls)
string lcT[40]; color lcC[40];
const int K_BAL=0,K_EQ=1,K_FLO=2,K_POS=3,K_TD=4,K_YD=5,K_WK=6,K_MO=7,K_LOTS=8,K_MODE=9,K_INFO=10,K_INFO2=11,
K_MSG=12,K_SELL=13,K_BUY=14,K_SPEED=15,K_SPR=16,K_MG=17,K_FM=18,K_ML=19,K_LI=20;
// --- speed slider
const double SPD_MIN=0.1, SPD_MAX=1000.0;
const int SL_TH=14, SL_H=24;
int slX=0, slY=0, slW=262;
bool sliderDrag = false;
// --- calendar popup
const int CAL_HDR=32, CAL_WK=22, CAL_CW=36, CAL_CH=28;
bool calOpen=false; int calTarget=0, calYear=2026, calMon=1;
int calX=0, calY=0, calW=7*36+16, calH=32+22+6*28+36;
int dateRowY=0, fromFieldX=0, toFieldX=0;
datetime calDate[42]; bool calOn[42];
datetime selFrom=0, selTo=0, dataFirst=0, dataLast=0; bool selDirty=false;
string MonName[12]={"January","February","March","April","May","June","July","August","September","October","November","December"};
// --- right-click context menu
bool ctxOpen=false; double ctxPrice=0; int ctxX=0, ctxY=0, ctxN=0;
string ctxId[4]; int ctxRY[4];
const int CTX_W=250, CTX_ROW=26;
// --- Trades window
bool trOpen=false; int trX=60, trY=120, trW=700, trH=250, trTop=0;
bool trDrag=false, trSize=false, trSizeDirty=false; int trDX=0, trDY=0; ulong trSizeUs=0;
const int TR_TITLE=24, TR_HEAD=28, TR_FOOT=36, TR_ROWH=22, TR_MAXROWS=40;
string twN[600]; int twOX[600], twOY[600]; int twCnt=0;
string twC[500]; color twCC[500];
int twKind[800], twIdx[800], twRows=0, twVis=0;
// --- stats
double dayProfit = 0, ydayProfit = 0, weekProfit = 0, monthProfit = 0;
double dayLots = 0, weekLots = 0, monthLots = 0;
datetime lastDay = 0, lastWeek = 0, lastMonth = 0;
double closedToday = 0;
// --- positions & orders
Pos pos[];
Ord ords[];
int openCnt = 0, ordLive = 0;
ulong nextT = 1;
// --- martingale
int martLevel = 0;
int martDir = 0;
double basketAvg = 0, basketLots = 0, basketSL = 0, basketTP = 0;
int bDir = 0;
bool basketTrailActive = false;
//+------------------------------------------------------------------+
//| Helpers |
//+------------------------------------------------------------------+
string CleanBase(string sym)
{
while(StringFind(sym,"REPLAY_")==0) sym = StringSubstr(sym,7);
StringReplace(sym,"+",""); StringReplace(sym,".","");
int len=StringLen(sym);
if(len>0){ string last=StringSubstr(sym,len-1,1);
if(last=="m"||last=="c"||last=="p") sym=StringSubstr(sym,0,len-1); }
if(StringLen(sym)<3) sym="XAUUSD";
return sym;
}
string TFName(ENUM_TIMEFRAMES tf)
{
if(tf==PERIOD_M1) return "M1"; if(tf==PERIOD_M5) return "M5";
if(tf==PERIOD_M15) return "M15"; if(tf==PERIOD_M30) return "M30";
if(tf==PERIOD_H1) return "H1"; if(tf==PERIOD_H4) return "H4";
if(tf==PERIOD_D1) return "D1"; if(tf==PERIOD_W1) return "W1"; return "M5";
}
string StatePath(){ return folderName+"\\state.bin"; }
double Rnd()
{
rngS ^= rngS >> 12;
rngS ^= rngS << 25;
rngS ^= rngS >> 27;
ulong r = rngS * 2685821657736338717;
return (double)(r >> 11) * (1.0/9007199254740992.0);
}
void SeedRng(ulong s)
{
rngS = s * 6364136223846793005 + 1442695040888963407;
if(rngS==0) rngS = 88172645463325252;
for(int i=0;i<4;i++) Rnd();
}
string Px(const double v){ return DoubleToString(v,digitsN); }
double CalcProfit(const int dir,const double open,const double lots,const double price)
{
double m = (dir>0) ? (price-open) : (open-price);
return NormalizeDouble(m * lots * InpContractSize, 2);
}
// MT5-style money text: 10 000.00
string Money(const double v)
{
string s=DoubleToString(MathAbs(v),2);
int dot=StringFind(s,".");
string ip=StringSubstr(s,0,dot), fp=StringSubstr(s,dot);
string o="";
int L=StringLen(ip);
for(int i=0;i<L;i++){ if(i>0 && (L-i)%3==0) o+=" "; o+=StringSubstr(ip,i,1); }
return (v<0?"-":"")+o+fp;
}
string MoneyS(const double v){ return (v>0?"+":"")+Money(v); }
color PnlCol(const double v){ return (v>0.004)?C'90,225,130':((v<-0.004)?C'255,100,100':clrWhite); }
//+------------------------------------------------------------------+
//| Init / Deinit |
//+------------------------------------------------------------------+
int OnInit()
{
panelChart = ChartID();
baseSym = CleanBase(_Symbol);
customSym = "REPLAY_"+baseSym;
folderName = "Custom_"+baseSym;
digitsN = (int)MathRound(-MathLog10(MathMax(InpPoint,1e-8)));
if(digitsN<0) digitsN=0; if(digitsN>8) digitsN=8;
spreadPx = InpSpreadPts*InpPoint;
lotSize = MathMax(InpLotMin,MathMin(InpLotMax,InpLot));
leverage = MathMax(1,InpLeverage);
resetBal = InpBalance;
slPts = InpSLpts;
tpPts = (InpTradeMode==MODE_SINGLE)?InpTPpts:InpMartTPpts;
trailOn = (InpTradeMode==MODE_SINGLE)?InpTrailing:(InpMartTrailStart>0);
trStart = (InpTradeMode==MODE_SINGLE)?InpTrailStart:InpMartTrailStart;
trStep = (InpTradeMode==MODE_SINGLE)?InpTrailStep:InpMartTrailStep;
ready=false;
Print("=== MarketReplay v9.10 | Mode: ",(InpTradeMode==MODE_SINGLE?"Single":"Martingale")," ===");
FolderCreate(folderName,0);
// --- Stage 1: broker symbol -> export all TFs, create custom symbol, switch chart
if(_Symbol!=customSym)
{
SaveSource();
if(!SetupCustom()){ Alert("Symbol error | خطا سمبل"); return INIT_FAILED; }
if(!ChartSetSymbolPeriod(panelChart,customSym,_Period))
Alert("Open a chart of ",customSym," and attach the EA there | چارت ",customSym," را باز و اکسپرت را روی آن بگذارید");
return INIT_SUCCEEDED;
}
// --- Stage 2: running on REPLAY_ symbol
if(!SetupCustom()){ Alert("Symbol error | خطا سمبل"); return INIT_FAILED; }
ArrayResize(tkOff,InpMaxTicks+16);
ArrayResize(tkBid,InpMaxTicks+16);
ArrayResize(wkb,InpMaxTicks+16);
ArrayResize(outTk,256);
if(!LoadBase()){ Alert("No data | داده‌ای یافت نشد"); return INIT_FAILED; }
barDurUs = (double)PeriodSeconds(usedTF)*1e6;
dataFirst = DayStart(rates[0].time); dataLast = DayStart(rates[total-1].time);
currentTF = (ENUM_TIMEFRAMES)_Period;
ChartSetInteger(panelChart,CHART_MODE,CHART_CANDLES);
ChartSetInteger(panelChart,CHART_SHOW_GRID,false);
ChartSetInteger(panelChart,CHART_AUTOSCROLL,true);
ChartSetInteger(panelChart,CHART_SHIFT,true);
ChartSetInteger(panelChart,CHART_SHOW_BID_LINE,true);
ChartSetInteger(panelChart,CHART_SHOW_ASK_LINE,true);
ChartSetInteger(panelChart,CHART_COLOR_BID,clrDodgerBlue);
ChartSetInteger(panelChart,CHART_COLOR_ASK,clrRed);
ChartSetInteger(panelChart,CHART_SHOW_OBJECT_DESCR,true);
ChartSetInteger(panelChart,CHART_CONTEXT_MENU,false); // our own right-click menu
ChartSetInteger(panelChart,CHART_EVENT_MOUSE_MOVE,true);
if(!TryRestoreState())
{
bal=InpBalance; eq=bal;
ResetTradeState(true);
endBar=total-1;
StartAt(MathMax(1,total/5));
selFrom=DayStart(rates[cur].time); selTo=dataLast; selDirty=false;
}
if(selFrom<=0) selFrom=DayStart(rates[MathMin(cur,total-1)].time);
if(selTo<=0) selTo=dataLast;
BuildPanel();
if(trOpen){ BuildTrades(); }
ready=true;
btnDirty=true; linesDirty=true;
lastUs=GetMicrosecondCount();
EventSetMillisecondTimer(playing?1:10);
UpdateUI();
Print("Ready | آماده | Bars: ",total," | Base TF: ",TFName(usedTF)," | Ticks/bar up to ",InpMaxTicks);
return INIT_SUCCEEDED;
}
void OnDeinit(const int reason)
{
EventKillTimer();
if(ready && reason==REASON_CHARTCHANGE) SaveState();
if(reason!=REASON_CHARTCHANGE)
{
ChartSetInteger(panelChart,CHART_CONTEXT_MENU,true);
ChartSetInteger(panelChart,CHART_EVENT_MOUSE_MOVE,false);
ObjectsDeleteAll(panelChart,"RPH_");
}
ChartSetInteger(panelChart,CHART_MOUSE_SCROLL,true);
ObjectsDeleteAll(panelChart,P);
ready=false;
}
//+------------------------------------------------------------------+
//| Timer: the heart of the replay |
//+------------------------------------------------------------------+
void OnTimer()
{
if(!ready) return;
ulong nowUs = GetMicrosecondCount();
if(playing)
{
double dt = (double)(nowUs - lastUs);
lastUs = nowUs;
if(dt > 1000000.0) dt = 1000000.0;
Advance(dt * speedMult);
}
else lastUs = nowUs;
if(trSizeDirty && (nowUs-trSizeUs)>=60000)
{
trSizeUs=nowUs; trSizeDirty=false;
BuildTrades(); RefreshTrades(); ChartRedraw(panelChart);
}
if(uiForce || (nowUs - lastUiUs) >= (ulong)InpUiMs*1000)
{
lastUiUs = nowUs;
UpdateUI();
}
}
//+------------------------------------------------------------------+
//| Advance the virtual clock by addUs and emit every due tick |
//+------------------------------------------------------------------+
void Advance(const double addUs)
{
if(tkN<=0) return;
ulong t0 = GetMicrosecondCount();
barClockUs += addUs;
outN = 0;
while(true)
{
while(tkIdx<tkN && tkOff[tkIdx]<=barClockUs)
{
EmitTick(tkIdx);
tkIdx++;
if(outN>=256) FlushTicks();
}
if(tkIdx>=tkN && barClockUs>=barDurUs)
{
if(cur>=endBar)
{
playing=false; btnDirty=true; barClockUs=barDurUs;
EventSetMillisecondTimer(10);
break;
}
barClockUs -= barDurUs; // carry remainder => zero drift
cur++;
UpdateStatsPeriod(rates[cur].time);
BuildBarTicks(cur);
tkIdx = 0;
if((double)(GetMicrosecondCount()-t0) > BUDGET_US) break; // keep UI responsive
continue;
}
break;
}
FlushTicks();
}
void EmitTick(const int i)
{
curBid = tkBid[i];
curAsk = curBid + spreadPx;
ulong msc = (ulong)rates[cur].time*1000 + (ulong)(tkOff[i]/1000.0);
MqlTick t;
t.time = (datetime)(msc/1000);
curTime = t.time;
t.time_msc = (long)msc;
t.bid = curBid;
t.ask = curAsk;
t.last = 0;
t.volume = 0;
t.volume_real = 0;
t.flags = TICK_FLAG_BID|TICK_FLAG_ASK;
outTk[outN++] = t;
if(openCnt>0 || ordLive>0) ManageTick(curBid,curAsk); // SL/TP/trailing on EVERY tick
}
void FlushTicks()
{
if(outN>0){ CustomTicksAdd(customSym,outTk,(uint)outN); outN=0; }
}
void BuildBarTicks(const int idx)
{
double o=rates[idx].open, h=rates[idx].high, l=rates[idx].low, c=rates[idx].close;
if(h<MathMax(o,c)) h=MathMax(o,c);
if(l>MathMin(o,c)) l=MathMin(o,c);
int minT=MathMax(4,InpMinTicks);
int maxT=MathMax(minT,InpMaxTicks);
int n=(int)rates[idx].tick_volume;
if(n<minT) n=minT;
if(n>maxT) n=maxT;
double rng=h-l;
SeedRng((ulong)rates[idx].time);
if(rng<=0.0)
{
n=2;
tkBid[0]=NormalizeDouble(o,digitsN);
tkBid[1]=NormalizeDouble(c,digitsN);
tkOff[0]=0; tkOff[1]=MathFloor(barDurUs*0.5/100.0)*100.0;
tkN=2; return;
}
double wp[4];
wp[0]=o;
if(c>=o){ wp[1]=l; wp[2]=h; } else { wp[1]=h; wp[2]=l; }
wp[3]=c;
// distribute ticks over the 3 legs proportionally to their length
int rem=n-1, m[3]; double len[3]; double S=0; int big=0;
for(int k=0;k<3;k++){ len[k]=MathAbs(wp[k+1]-wp[k]); S+=len[k]; if(len[k]>len[big]) big=k; }
int extra=rem-3, used=0;
for(int k=0;k<3;k++){ m[k]=1+(int)MathFloor(extra*len[k]/S); used+=m[k]; }
m[big]+=rem-used;
tkBid[0]=NormalizeDouble(o,digitsN);
int w=1;
for(int k=0;k<3;k++)
{
int mk=m[k]; double p0=wp[k], p1=wp[k+1];
double sigma=InpPathNoise*rng/MathSqrt((double)mk);
double acc=0;
for(int s=1;s<=mk;s++){ acc+=(Rnd()*2.0-1.0)*1.7320508*sigma; wkb[s]=acc; }
double wm=wkb[mk];
for(int s=1;s<=mk;s++)
{
double px;
if(s==mk) px=p1; // legs end exactly on O/H/L/C
else
{
px=p0+(p1-p0)*s/mk+(wkb[s]-wm*s/mk); // Brownian bridge
if(px>h) px=h;
if(px<l) px=l;
}
tkBid[w++]=NormalizeDouble(px,digitsN);
}
}
// Poisson (exponential) inter-arrival times, snapped to 100us
tkOff[0]=0;
double cum=0;
for(int i=1;i<=n;i++)
{
double u=Rnd(); if(u<1e-12) u=1e-12;
cum+=-MathLog(u);
if(i<n) tkOff[i]=cum;
}
double scale=barDurUs*0.999/cum;
for(int i=1;i<n;i++) tkOff[i]=MathFloor(tkOff[i]*scale/100.0)*100.0;
tkN=n;
}
//+------------------------------------------------------------------+
//| Per-tick trade management |
//+------------------------------------------------------------------+
void ManageTick(const double bid,const double ask)
{
const double pt=InpPoint;
if(ordLive>0) CheckOrders(bid,ask);
if(openCnt>0 && StopOutHit()){ DoStopOut(); return; }
if(InpTradeMode==MODE_SINGLE)
{
for(int i=ArraySize(pos)-1;i>=0;i--)
{
if(!pos[i].open_) continue;
if(pos[i].dir>0)
{
if(pos[i].sl>0 && bid<=pos[i].sl){ ClosePos(i,bid); continue; }
if(pos[i].tp>0 && bid>=pos[i].tp){ ClosePos(i,pos[i].tp); continue; }
if(trailOn && trStart>0 && (bid-pos[i].open)/pt>=trStart)
{
double ns=NormalizeDouble(bid-trStep*pt,digitsN);
if(ns>pos[i].sl){ pos[i].sl=ns; linesDirty=true; }
}
}
else
{
if(pos[i].sl>0 && ask>=pos[i].sl){ ClosePos(i,ask); continue; }
if(pos[i].tp>0 && ask<=pos[i].tp){ ClosePos(i,pos[i].tp); continue; }
if(trailOn && trStart>0 && (pos[i].open-ask)/pt>=trStart)
{
double ns=NormalizeDouble(ask+trStep*pt,digitsN);
if(pos[i].sl==0 || ns<pos[i].sl){ pos[i].sl=ns; linesDirty=true; }
}
}
}
return;
}
// ===== Martingale basket =====
if(basketLots<=0 || bDir==0) return;
if(bDir>0)
{
if(bid>=basketTP){ CloseBasket(basketTP); return; }
if(trailOn && trStart>0)
{
double pts=(bid-basketAvg)/pt;
if(!basketTrailActive && pts>=trStart)
{ basketTrailActive=true; basketSL=NormalizeDouble(bid-trStep*pt,digitsN); linesDirty=true; }
if(basketTrailActive)
{
double ns=NormalizeDouble(bid-trStep*pt,digitsN);
if(ns>basketSL){ basketSL=ns; linesDirty=true; }
if(bid<=basketSL){ CloseBasket(bid); return; }
}
}
}
else
{
if(ask<=basketTP){ CloseBasket(basketTP); return; }
if(trailOn && trStart>0)
{
double pts=(basketAvg-ask)/pt;
if(!basketTrailActive && pts>=trStart)
{ basketTrailActive=true; basketSL=NormalizeDouble(ask+trStep*pt,digitsN); linesDirty=true; }
if(basketTrailActive)
{
double ns=NormalizeDouble(ask+trStep*pt,digitsN);
if(ns<basketSL || basketSL==0){ basketSL=ns; linesDirty=true; }
if(ask>=basketSL){ CloseBasket(ask); return; }
}
}
}
}
string OrdTypeName(const int t)
{
if(t==1) return "buy limit"; if(t==2) return "sell limit";
if(t==3) return "buy stop"; return "sell stop";
}
// pending orders trigger on Ask (buy) / Bid (sell), exactly like a real server
void CheckOrders(const double bid,const double ask)
{
for(int i=0;i<ArraySize(ords);i++)
{
if(!ords[i].live) continue;
int t=ords[i].type; double pr=ords[i].price; double fp=pr; bool hit=false;
if(t==1 && ask<=pr){ hit=true; fp=MathMin(pr,ask); }
else if(t==3 && ask>=pr){ hit=true; fp=ask; }
else if(t==2 && bid>=pr){ hit=true; fp=MathMax(pr,bid); }
else if(t==4 && bid<=pr){ hit=true; fp=bid; }
if(!hit) continue;
ords[i].live=false; ordLive--; ordCompact=true; linesDirty=true;
int dir=(t==1||t==3)?1:-1;
if(!OpenPos(dir,fp,ords[i].lots,ords[i].ticket))
pendLog+=StringFormat("Pending #%I64u cancelled (blocked)\n",ords[i].ticket);
else
pendLog+=StringFormat("Pending #%I64u %s filled @ %s\n",ords[i].ticket,OrdTypeName(t),Px(fp));
}
}
void ClosePos(const int i,const double price)
{
double pr=CalcProfit(pos[i].dir,pos[i].open,pos[i].lots,price);
bal+=pr; dayProfit+=pr; weekProfit+=pr; monthProfit+=pr; closedToday+=pr;
AddHevClose(pos[i],price,pr);
pos[i].open_=false; openCnt--;
needCompact=true; linesDirty=true;
pendLog+=StringFormat("Closed #%I64u %s @ %s P/L=%+.2f\n",pos[i].ticket,(pos[i].dir>0?"BUY":"SELL"),Px(price),pr);
}
void CloseBasket(const double price)
{
double totalPr=0;
for(int i=0;i<ArraySize(pos);i++)
{
if(!pos[i].open_) continue;
double pp=CalcProfit(pos[i].dir,pos[i].open,pos[i].lots,price);
totalPr+=pp;
AddHevClose(pos[i],price,pp);
pos[i].open_=false;
}
openCnt=0; needCompact=true; linesDirty=true;
bal+=totalPr; dayProfit+=totalPr; weekProfit+=totalPr; monthProfit+=totalPr; closedToday+=totalPr;
if(totalPr<0) martLevel=MathMin(martLevel+1,InpMartMaxLevels);
else { martLevel=0; martDir=0; }
basketAvg=0; basketLots=0; basketSL=0; basketTP=0; bDir=0; basketTrailActive=false;
pendLog+=StringFormat("Basket closed @ %s P/L=%+.2f | Next level=%d\n",Px(price),totalPr,martLevel);
}
void CalcBasketAvg()
{
basketAvg=0; basketLots=0; bDir=0;
double sum=0;
for(int i=0;i<ArraySize(pos);i++)
{
if(!pos[i].open_) continue;
if(bDir==0) bDir=pos[i].dir;
sum+=pos[i].open*pos[i].lots;
basketLots+=pos[i].lots;
}
if(basketLots>0)
{
basketAvg=NormalizeDouble(sum/basketLots,digitsN);
basketTP=NormalizeDouble(basketAvg+bDir*tpPts*InpPoint,digitsN);
}
else { basketSL=0; basketTP=0; basketTrailActive=false; }
}
//+------------------------------------------------------------------+
//| Leverage / margin / stop-out / balance control |
//+------------------------------------------------------------------+
void Account(double &flo,double &used)
{
flo=0; used=0;
for(int i=0;i<ArraySize(pos);i++)
{
if(!pos[i].open_) continue;
flo+=CalcProfit(pos[i].dir,pos[i].open,pos[i].lots,pos[i].dir>0?curBid:curAsk);
used+=pos[i].lots*InpContractSize*pos[i].open/leverage;
}
}
double FreeMargin()
{
double f,u; Account(f,u);
return bal+f-u;
}
// largest lot (on lot-step grid) that the free margin can afford at this price
double MaxLotFor(const double freeM,const double price)
{
if(freeM<=0 || price<=0) return 0.0;
double lot=freeM*leverage/(InpContractSize*price);
double st=MathMax(InpLotStep,0.0001);
lot=MathFloor(lot/st+1e-9)*st;
lot=MathMin(lot,InpLotMax);
if(lot<InpLotMin) return 0.0;
return NormalizeDouble(lot,2);
}
bool StopOutHit()
{
double f,u; Account(f,u);
if(u<=0) return false;
return ((bal+f)/u*100.0 < InpStopOutPct);
}
void DoStopOut()
{
double f,u; Account(f,u);
pendLog+=StringFormat("STOP OUT: margin level %.1f%% < %.0f%% | equity %s\n",(bal+f)/u*100.0,InpStopOutPct,Money(bal+f));
CloseAll();
for(int i=0;i<ArraySize(ords);i++) CancelOrder(i);
martLevel=0; martDir=0;
if(InpAutoReset)
{
bal=resetBal;
lastMsg="STOP OUT - balance reset to "+Money(resetBal)+" | بالانس ریست شد";
}
else
{
lastMsg="STOP OUT - use Reset balance or + Deposit | مارجین کال";
playing=false; btnDirty=true; EventSetMillisecondTimer(10);
}
uiForce=true;
}
void ResetBalance()
{
CloseAll();
for(int i=0;i<ArraySize(ords);i++) CancelOrder(i);
martLevel=0; martDir=0;
bal=resetBal;
dayProfit=0; ydayProfit=0; weekProfit=0; monthProfit=0;
dayLots=0; weekLots=0; monthLots=0; closedToday=0;
lastMsg="Balance reset to "+Money(resetBal)+" | بالانس ریست شد";
uiForce=true; linesDirty=true;
}
void Deposit()
{
bal+=resetBal;
lastMsg="Deposited "+Money(resetBal)+" | واریز شد";
uiForce=true;
}
void ApplyLevText()
{
string t=ObjectGetString(panelChart,P+"LEV",OBJPROP_TEXT);
StringReplace(t,"1:",""); StringReplace(t," ","");
long v=StringToInteger(t);
if(v<1) v=leverage;
if(v>5000) v=5000;
leverage=(int)v;
ObjectSetString(panelChart,P+"LEV",OBJPROP_TEXT,IntegerToString(leverage));
lastMsg="Leverage set to 1:"+IntegerToString(leverage);
uiForce=true;
}
void ApplySLTPText()
{
long s=StringToInteger(ObjectGetString(panelChart,P+"SLP",OBJPROP_TEXT));
long t=StringToInteger(ObjectGetString(panelChart,P+"TPP",OBJPROP_TEXT));
if(s<0) s=0; if(t<0) t=0;
slPts=(int)s; tpPts=(int)t;
ObjectSetString(panelChart,P+"SLP",OBJPROP_TEXT,IntegerToString(slPts));
ObjectSetString(panelChart,P+"TPP",OBJPROP_TEXT,IntegerToString(tpPts));
lastMsg=StringFormat("SL=%d TP=%d points (new trades)",slPts,tpPts);
uiForce=true;
}
void ApplyBalText()
{
string t=ObjectGetString(panelChart,P+"BALV",OBJPROP_TEXT);
StringReplace(t,",","."); StringReplace(t," ","");
double v=StringToDouble(t);
if(v<=0) v=resetBal;
resetBal=NormalizeDouble(v,2);
ObjectSetString(panelChart,P+"BALV",OBJPROP_TEXT,DoubleToString(resetBal,2));
uiForce=true;
}
void ApplySLTPOpen()
{
if(InpTradeMode==MODE_SINGLE)
{
for(int i=0;i<ArraySize(pos);i++)
{
if(!pos[i].open_) continue;
pos[i].sl=slPts>0?NormalizeDouble(pos[i].open-pos[i].dir*slPts*InpPoint,digitsN):0;
pos[i].tp=tpPts>0?NormalizeDouble(pos[i].open+pos[i].dir*tpPts*InpPoint,digitsN):0;
}
}
else { double sl=basketSL; bool ta=basketTrailActive; CalcBasketAvg(); basketSL=sl; basketTrailActive=ta; }
linesDirty=true; uiForce=true;
lastMsg="SL/TP applied to open positions | اعمال شد";
}
bool IsEditName(const string n)
{
return (n==P+"LOT"||n==P+"LEV"||n==P+"SLP"||n==P+"TPP"||n==P+"BALV"||n==P+"TRS"||n==P+"TRP"||n==P+"MOD_SL"||n==P+"MOD_TP"||n==P+"VOL_V");
}
//+------------------------------------------------------------------+
//| BUY / SELL / pending - hot path, no UI work |
//+------------------------------------------------------------------+
bool OpenPos(const int dir,const double price,const double baseLots,const ulong useTicket)
{
double lots=baseLots; int level=0;
if(InpTradeMode==MODE_MARTINGALE)
{
if(openCnt>0 && martDir!=0 && martDir!=dir)
{ lastMsg="Opposite blocked - close basket first | جهت مخالف ممکن نیست، ابتدا سبد را ببندید"; uiForce=true; return false; }
if(openCnt==0 && martDir!=dir) martLevel=0;
martDir=dir;
level=martLevel;
int lv=MathMin(martLevel,InpMartMaxLevels);
lots=NormalizeDouble(baseLots*MathPow(InpMartMultiplier,lv),2);
}
double need=lots*InpContractSize*price/leverage;
double fm=FreeMargin();
if(need>fm+1e-9)
{
lastMsg=StringFormat("Not enough margin: need %s, free %s | مارجین کافی نیست",Money(need),Money(fm));
uiForce=true; return false;
}
Pos p;
p.ticket=(useTicket>0)?useTicket:nextT++;
p.dir=dir; p.lots=lots; p.open=price;
p.time=rates[cur].time; p.open_=true; p.level=level;
if(InpTradeMode==MODE_SINGLE)
{
p.sl=slPts>0 ? NormalizeDouble(price-dir*slPts*InpPoint,digitsN) : 0;
p.tp=tpPts>0 ? NormalizeDouble(price+dir*tpPts*InpPoint,digitsN) : 0;
}
else { p.sl=0; p.tp=0; }
int n=ArraySize(pos);
ArrayResize(pos,n+1,64);
pos[n]=p;
openCnt++;
AddHevOpen(p);
dayLots+=lots; weekLots+=lots; monthLots+=lots;
if(InpTradeMode==MODE_MARTINGALE){ CalcBasketAvg(); basketTrailActive=false; basketSL=0; }
linesDirty=true; uiForce=true;
lastMsg="";
pendLog+=StringFormat("%s #%I64u Lot=%.2f @ %s\n",(dir>0?"BUY":"SELL"),p.ticket,lots,Px(price));
return true;
}
void OpenTrade(const int dir)
{
if(curBid<=0) return;
OpenPos(dir,(dir>0)?curAsk:curBid,lotSize,0);
}
void PlaceOrder(const int type,const double price0)
{
double price=NormalizeDouble(price0,digitsN);
bool ok=false;
if(type==1) ok=(price<curAsk);
else if(type==2) ok=(price>curBid);
else if(type==3) ok=(price>curAsk);
else if(type==4) ok=(price<curBid);
if(!ok){ lastMsg="Invalid price for "+OrdTypeName(type)+" | قیمت نامعتبر"; uiForce=true; return; }
Ord o;
o.ticket=nextT++; o.type=type; o.price=price; o.lots=lotSize;
o.time=rates[cur].time; o.live=true;
int n=ArraySize(ords);
ArrayResize(ords,n+1,16);
ords[n]=o;
ordLive++;
linesDirty=true; uiForce=true;
lastMsg=StringFormat("Placed %s %.2f @ %s",OrdTypeName(type),lotSize,Px(price));
pendLog+=StringFormat("Order #%I64u %s %.2f @ %s\n",o.ticket,OrdTypeName(type),lotSize,Px(price));
}
void CancelOrder(const int i)
{
if(i<0 || i>=ArraySize(ords) || !ords[i].live) return;
ords[i].live=false; ordLive--; ordCompact=true; linesDirty=true; uiForce=true;
pendLog+=StringFormat("Order #%I64u deleted\n",ords[i].ticket);
}
void CloseAll()
{
if(openCnt<=0) return;
if(InpTradeMode==MODE_SINGLE)
{
for(int i=ArraySize(pos)-1;i>=0;i--)
if(pos[i].open_) ClosePos(i,pos[i].dir>0?curBid:curAsk);
}
else CloseBasket(bDir>0?curBid:curAsk);
uiForce=true;
}
void SetLot(double v)
{
v=MathMax(InpLotMin,MathMin(InpLotMax,v));
double st=MathMax(InpLotStep,0.0001);
v=NormalizeDouble(MathRound(v/st)*st,2);
if(v<InpLotMin) v=InpLotMin;
lotSize=v;
ObjectSetString(panelChart,P+"LOT",OBJPROP_TEXT,DoubleToString(v,2));
uiForce=true;
}
void ApplyLotText()
{
string t=ObjectGetString(panelChart,P+"LOT",OBJPROP_TEXT);
StringReplace(t,",","."); StringTrimLeft(t); StringTrimRight(t);
double v=StringToDouble(t);
if(v<=0) v=lotSize;
SetLot(v);
}
//+------------------------------------------------------------------+
//| Events - everything fires on mouse-DOWN |
//+------------------------------------------------------------------+
string HitPanel(const int x,const int y)
{
for(int i=0;i<rN;i++)
if(x>=rX[i] && x<rX[i]+rW[i] && y>=rY[i] && y<rY[i]+rH[i]) return rId[i];
return "";
}
bool InCal(const int x,const int y)
{
return calOpen && x>=calX && x<calX+calW && y>=calY && y<calY+calH;
}
string HitCal(const int x,const int y)
{
if(x>=calX+6 && x<calX+34 && y>=calY+4 && y<calY+28) return "CAL_PREV";
if(x>=calX+calW-34 && x<calX+calW-6 && y>=calY+4 && y<calY+28) return "CAL_NEXT";
int gx=calX+8, gy=calY+CAL_HDR+CAL_WK;
if(x>=gx && x<gx+7*CAL_CW && y>=gy && y<gy+6*CAL_CH)
return "CAL_D"+IntegerToString(((y-gy)/CAL_CH)*7+(x-gx)/CAL_CW);
if(x>=gx && x<gx+210 && y>=gy+6*CAL_CH+4 && y<gy+6*CAL_CH+28) return "CAL_TODAY";
return "";
}
void MarkPressed(const string id){ pressId=id; pressMs=GetTickCount64(); }
bool InTrades(const int x,const int y)
{
return trOpen && x>=trX && x<trX+trW && y>=trY && y<trY+trH;
}
//--- speed slider ---------------------------------------------------
double SpeedFromPos(const double p)
{
double s=SPD_MIN*MathPow(SPD_MAX/SPD_MIN,p);
if(MathAbs(MathLog10(s))<0.025) return 1.0; // snap to Live
if(s>=10) return MathRound(s);
if(s>=1) return MathRound(s*10.0)/10.0;
return MathRound(s*100.0)/100.0;
}
double PosFromSpeed(const double s)
{
double p=MathLog10(MathMax(s,SPD_MIN)/SPD_MIN)/MathLog10(SPD_MAX/SPD_MIN);
return MathMax(0.0,MathMin(1.0,p));
}
string SpeedTxt(const double s)
{
if(s==1.0) return "Live 1x";
if(s>=10) return DoubleToString(s,0)+"x";
if(s>=1) return DoubleToString(s,1)+"x";
return DoubleToString(s,2)+"x";
}
void PlaceThumb()
{
double p=PosFromSpeed(speedMult);
ObjectSetInteger(panelChart,P+"SL_TH",OBJPROP_XDISTANCE,slX+(int)MathRound(p*(slW-SL_TH)));
}
void SetSpeed(const double s)
{
speedMult=MathMax(SPD_MIN,MathMin(SPD_MAX,s));
PlaceThumb(); btnDirty=true; uiForce=true;
}
void SliderAt(const int x)
{
double p=(double)(x-slX-SL_TH/2)/(double)(slW-SL_TH);
SetSpeed(SpeedFromPos(MathMax(0.0,MathMin(1.0,p))));
}
void SpeedStep(const int dir)
{
double p=PosFromSpeed(speedMult)+dir*0.03;
SetSpeed(SpeedFromPos(MathMax(0.0,MathMin(1.0,p))));
}
//--- right-click context menu (Limit / Stop orders) ------------------
void CloseCtx()
{
if(!ctxOpen) return;
ObjectsDeleteAll(panelChart,P+"CTX_");
ctxOpen=false;
ChartRedraw(panelChart);
}
string HitCtx(const int x,const int y)
{
if(!ctxOpen) return "";
for(int i=0;i<ctxN;i++)
if(x>=ctxX+3 && x<ctxX+CTX_W-3 && y>=ctxRY[i] && y<ctxRY[i]+CTX_ROW-2) return ctxId[i];
return "";
}
void OpenCtx(const int x,const int y)
{
if(curBid<=0) return;
int sub=0; datetime t=0; double price=0;
if(!ChartXYToTimePrice(panelChart,x,y,sub,t,price) || sub!=0) return;
price=NormalizeDouble(price,digitsN);
CloseCtx(); CloseCal();
ctxPrice=price; ctxN=0;
if(price<curBid) { ctxId[0]="CTX_BL"; ctxId[1]="CTX_SS"; ctxN=2; }
else if(price>curAsk) { ctxId[0]="CTX_SLM"; ctxId[1]="CTX_BS"; ctxN=2; }
else { lastMsg="Too close to market price | قیمت خیلی نزدیک بازار است"; uiForce=true; return; }
int w=CTX_W, h=CTX_ROW*(ctxN+1)+6;
int cw=(int)ChartGetInteger(panelChart,CHART_WIDTH_IN_PIXELS);
int ch=(int)ChartGetInteger(panelChart,CHART_HEIGHT_IN_PIXELS);
ctxX=MathMax(2,MathMin(x,cw-w-4)); ctxY=MathMax(2,MathMin(y,ch-h-4));
Rect("CTX_BG",ctxX,ctxY,w,h,C'30,36,54',C'0,150,255',2999);
RawBtn(P+"CTX_H",StringFormat("Order @ %s | Lot %.2f",Px(price),lotSize),ctxX+3,ctxY+3,w-6,CTX_ROW-2,
C'0,92,170',clrWhite,C'0,92,170',9,3000);
for(int i=0;i<ctxN;i++)
{
ctxRY[i]=ctxY+3+CTX_ROW*(i+1);
bool buy=(ctxId[i]=="CTX_BL"||ctxId[i]=="CTX_BS");
string nm=(ctxId[i]=="CTX_BL")?"Buy Limit":(ctxId[i]=="CTX_SS")?"Sell Stop":(ctxId[i]=="CTX_SLM")?"Sell Limit":"Buy Stop";
color bg=buy?clrForestGreen:clrFireBrick;
RawBtn(P+ctxId[i],nm+" "+DoubleToString(lotSize,2)+" @ "+Px(price),ctxX+3,ctxRY[i],w-6,CTX_ROW-2,
bg,clrWhite,Lighten(bg,55),9,3000);
}
ctxOpen=true;
ChartRedraw(panelChart);
}
void RightClickAt(const int x,const int y)
{
if(x>=4 && x<4+panW && y>=4 && y<4+panH) return; // not over the panel
if(InTrades(x,y)) return;
OpenCtx(x,y);
}
//--- Trades window interaction ---------------------------------------
void CloseRow(const int row)
{
if(row<0 || row>=twRows) return;
if(twKind[row]==0)
{
int i=twIdx[row];
if(i>=0 && i<ArraySize(pos) && pos[i].open_)
{
if(InpTradeMode==MODE_SINGLE) ClosePos(i,pos[i].dir>0?curBid:curAsk);
else CloseBasket(bDir>0?curBid:curAsk);
}
}
else CancelOrder(twIdx[row]);
uiForce=true;
}
void MoveTrades()
{
for(int i=0;i<twCnt;i++)
{
ObjectSetInteger(panelChart,twN[i],OBJPROP_XDISTANCE,trX+twOX[i]);
ObjectSetInteger(panelChart,twN[i],OBJPROP_YDISTANCE,trY+twOY[i]);
}
}
void HandleTrades(const int x,const int y)
{
if(y<trY+TR_TITLE)
{
if(x>=trX+trW-28){ Act("TW_CLOSE"); return; }
trDrag=true; trDX=x-trX; trDY=y-trY;
ChartSetInteger(panelChart,CHART_MOUSE_SCROLL,false);
return;
}
if(y>=trY+trH-TR_FOOT)
{
if(x>=trX+trW-22){ trSize=true; ChartSetInteger(panelChart,CHART_MOUSE_SCROLL,false); return; }
if(x>=trX+trW-78 && x<trX+trW-52){ trTop=MathMax(0,trTop-1); RefreshTrades(); ChartRedraw(panelChart); return; }
if(x>=trX+trW-52 && x<trX+trW-26){ trTop++; RefreshTrades(); ChartRedraw(panelChart); return; }
return;
}
int rowsTop=trY+TR_TITLE+TR_HEAD;
if(y>=rowsTop && x>=trX+trW-30)
{
int r=(y-rowsTop)/TR_ROWH;
if(r>=0 && r<twVis) CloseRow(trTop+r);
return;
}
if(y>=rowsTop)
{
int r=(y-rowsTop)/TR_ROWH;
if(r<0 || r>=twVis || trTop+r>=twRows) return;
double tw=(double)(trW-2-30), acc=1.0;
for(int c=0;c<10;c++)
{
double l=acc; acc+=twFr[c]*tw;
if((c==6||c==7) && x>=trX+(int)l && x<trX+(int)acc)
{ if(twKind[trTop+r]==0) OpenMod(twIdx[trTop+r]); return; }
if(c==4 && x>=trX+(int)l && x<trX+(int)acc)
{ if(twKind[trTop+r]==0) OpenVol(twIdx[trTop+r]); return; }
}
}
}
//+------------------------------------------------------------------+
//| Trade history marks: MT5-style arrows + dotted entry->exit lines |
//+------------------------------------------------------------------+
void AddHevOpen(const Pos &p)
{
int n=ArraySize(hev);
if(n>3000) return;
ArrayResize(hev,n+1,32);
hev[n].ticket=p.ticket; hev[n].kind=0; hev[n].dir=p.dir;
hev[n].t=p.time; hev[n].price=p.open; hev[n].t0=p.time; hev[n].p0=p.open; hev[n].profit=0;
hev[n].part=0; hev[n].lots=p.lots;
}
void AddHevClose(const Pos &p,const double price,const double profit)
{
int n=ArraySize(hev);
if(n>3000) return;
ArrayResize(hev,n+1,32);
hev[n].ticket=p.ticket; hev[n].kind=1; hev[n].dir=p.dir;
hev[n].t=(curTime>0)?curTime:p.time; hev[n].price=price;
hev[n].t0=p.time; hev[n].p0=p.open; hev[n].profit=profit;
hev[n].part=0; hev[n].lots=p.lots;
}
// partial close: same exit mark + dotted line as a full close, but a unique object name per event
void AddHevPartial(const Pos &p,const double closedLots,const double price,const double profit)
{
int n=ArraySize(hev);
if(n>3000) return;
ArrayResize(hev,n+1,32);
hev[n].ticket=p.ticket; hev[n].kind=1; hev[n].dir=p.dir;
hev[n].t=(curTime>0)?curTime:p.time; hev[n].price=price;
hev[n].t0=p.time; hev[n].p0=p.open; hev[n].profit=profit;
hev[n].part=++partSeq; hev[n].lots=closedLots;
}
void ProcessHistory()
{
int n=ArraySize(hev);
if(n<=0) return;
for(int k=0;k<n;k++)
{
string base="RPH_"+IntegerToString((long)hev[k].ticket)+(hev[k].part>0?"p"+IntegerToString(hev[k].part):"");
bool buy=(hev[k].dir>0);
color col=buy?clrDodgerBlue:clrRed;
if(hev[k].kind==0)
{
string nm=base+"_in";
if(ObjectCreate(panelChart,nm,buy?OBJ_ARROW_BUY:OBJ_ARROW_SELL,0,hev[k].t,hev[k].price))
{
ObjectSetInteger(panelChart,nm,OBJPROP_COLOR,col);
ObjectSetInteger(panelChart,nm,OBJPROP_SELECTABLE,false);
ObjectSetString(panelChart,nm,OBJPROP_TOOLTIP,"#"+IntegerToString((long)hev[k].ticket)+(buy?" buy @ ":" sell @ ")+Px(hev[k].price));
}
}
else
{
string nx=base+"_out", nl=base+"_ln";
color pc=(hev[k].profit>=0)?clrLimeGreen:clrOrangeRed;
if(ObjectCreate(panelChart,nx,OBJ_ARROW,0,hev[k].t,hev[k].price))
{
ObjectSetInteger(panelChart,nx,OBJPROP_ARROWCODE,251);
ObjectSetInteger(panelChart,nx,OBJPROP_COLOR,pc);
ObjectSetInteger(panelChart,nx,OBJPROP_SELECTABLE,false);
ObjectSetString(panelChart,nx,OBJPROP_TOOLTIP,"#"+IntegerToString((long)hev[k].ticket)+(hev[k].part>0?" partial close "+DoubleToString(hev[k].lots,2)+" @ ":" closed @ ")+Px(hev[k].price)+" "+MoneyS(hev[k].profit));
}
if(ObjectCreate(panelChart,nl,OBJ_TREND,0,hev[k].t0,hev[k].p0,hev[k].t,hev[k].price))
{
ObjectSetInteger(panelChart,nl,OBJPROP_COLOR,col);
ObjectSetInteger(panelChart,nl,OBJPROP_STYLE,STYLE_DOT);
ObjectSetInteger(panelChart,nl,OBJPROP_WIDTH,1);
ObjectSetInteger(panelChart,nl,OBJPROP_RAY_RIGHT,false);
ObjectSetInteger(panelChart,nl,OBJPROP_SELECTABLE,false);
ObjectSetInteger(panelChart,nl,OBJPROP_BACK,true);
}
int m=ArraySize(hDrawn);
ArrayResize(hDrawn,m+1,64);
hDrawn[m]=base;
if(m+1>300) // keep the chart light
{
string ob=hDrawn[0];
ObjectDelete(panelChart,ob+"_in"); ObjectDelete(panelChart,ob+"_out"); ObjectDelete(panelChart,ob+"_ln");
ArrayRemove(hDrawn,0,1);
}
}
}
ArrayResize(hev,0);
}
//+------------------------------------------------------------------+
//| Modify SL / TP: validation, drag on chart, Modify popup |
//+------------------------------------------------------------------+
int FindPosByTicket(const ulong tk)
{
for(int i=0;i<ArraySize(pos);i++) if(pos[i].open_ && pos[i].ticket==tk) return i;
return -1;
}
bool SetPosLevel(const int i,const bool isSL,const double price0,string &err)
{
err="";
if(InpTradeMode!=MODE_SINGLE){ err="Martingale: SL/TP belong to the basket | در مارتینگل SL/TP برای کل سبد است"; return false; }
double price=(price0<=0)?0.0:NormalizeDouble(price0,digitsN);
if(price>0)
{
bool buy=(pos[i].dir>0);
bool ok;
if(isSL) ok=buy?(price<curBid):(price>curAsk);
else ok=buy?(price>curBid):(price<curAsk);
if(!ok){ err=(isSL?"Invalid SL level":"Invalid TP level"); err+=" | سطح نامعتبر"; return false; }
}
if(isSL) pos[i].sl=price; else pos[i].tp=price;
linesDirty=true; uiForce=true;
return true;
}
int PriceToY(const double price)
{
double mx=ChartGetDouble(panelChart,CHART_PRICE_MAX,0), mn=ChartGetDouble(panelChart,CHART_PRICE_MIN,0);
int h=(int)ChartGetInteger(panelChart,CHART_HEIGHT_IN_PIXELS,0);
if(mx<=mn || h<=0) return -10000;
return (int)MathRound((mx-price)/(mx-mn)*h);
}
double YToPrice(const int y)
{
double mx=ChartGetDouble(panelChart,CHART_PRICE_MAX,0), mn=ChartGetDouble(panelChart,CHART_PRICE_MIN,0);
int h=(int)ChartGetInteger(panelChart,CHART_HEIGHT_IN_PIXELS,0);
if(h<=0) return 0;
return mx-(double)y/(double)h*(mx-mn);
}
void BeginDrag(const int kind,const ulong tk,const string name)
{
dragKind=kind; dragTicket=tk; dragName=name;
dragPrice=ObjectGetDouble(panelChart,name,OBJPROP_PRICE);
dragPrice0=dragPrice;
ObjectSetInteger(panelChart,name,OBJPROP_WIDTH,2);
ChartSetInteger(panelChart,CHART_MOUSE_SCROLL,false);
}
// press on a SL / TP / pending-order line -> drag it like in MetaTrader
bool TryStartLineDrag(const int x,const int y)
{
if(dragKind!=0) return false;
const int TOL=5;
if(InpTradeMode==MODE_SINGLE)
{
for(int i=0;i<ArraySize(pos);i++)
{
if(!pos[i].open_) continue;
string t=IntegerToString((long)pos[i].ticket);
if(pos[i].sl>0 && MathAbs(y-PriceToY(pos[i].sl))<=TOL){ BeginDrag(1,pos[i].ticket,P+"S_"+t); return true; }
if(pos[i].tp>0 && MathAbs(y-PriceToY(pos[i].tp))<=TOL){ BeginDrag(2,pos[i].ticket,P+"T_"+t); return true; }
}
}
else if(basketLots>0 && basketTP>0 && MathAbs(y-PriceToY(basketTP))<=TOL)
{ BeginDrag(4,0,P+"BTP"); return true; }
for(int i=0;i<ArraySize(ords);i++)
{
if(!ords[i].live) continue;
if(MathAbs(y-PriceToY(ords[i].price))<=TOL){ BeginDrag(3,ords[i].ticket,P+"O_"+IntegerToString((long)ords[i].ticket)); return true; }
}
return false;
}
void DragMove(const int y)
{
double p=NormalizeDouble(YToPrice(y),digitsN);
if(p<=0) return;
dragPrice=p;
ObjectSetDouble(panelChart,dragName,OBJPROP_PRICE,p);
ChartRedraw(panelChart);
}
void ApplyDrag()
{
int kind=dragKind; dragKind=0;
ObjectSetInteger(panelChart,dragName,OBJPROP_WIDTH,(dragName==P+"BTP")?2:1);
linesDirty=true; uiForce=true; // redraw from data: reverts the line if the move is refused
double np=NormalizeDouble(dragPrice,digitsN);
if(MathAbs(np-dragPrice0)<InpPoint*0.5) return;
string e="";
if(kind==1 || kind==2)
{
int i=FindPosByTicket(dragTicket);
if(i<0) return;
if(SetPosLevel(i,kind==1,np,e)) lastMsg=StringFormat("#%I64u %s moved to %s",dragTicket,(kind==1?"SL":"TP"),Px(np));
else lastMsg=e;
}
else if(kind==3)
{
for(int i=0;i<ArraySize(ords);i++)
{
if(!ords[i].live || ords[i].ticket!=dragTicket) continue;
int t=ords[i].type; bool ok=false;
if(t==1) ok=(np<curAsk); else if(t==2) ok=(np>curBid); else if(t==3) ok=(np>curAsk); else ok=(np<curBid);
if(ok){ ords[i].price=np; lastMsg=StringFormat("Order #%I64u moved to %s",dragTicket,Px(np)); }
else lastMsg="Invalid price for "+OrdTypeName(t)+" | قیمت نامعتبر";
break;
}
}
else if(kind==4)
{
bool ok=(bDir>0)?(np>basketAvg):(np<basketAvg);
int pts=(int)MathRound(MathAbs(np-basketAvg)/InpPoint);
if(ok && pts>=1)
{
tpPts=pts;
ObjectSetString(panelChart,P+"TPP",OBJPROP_TEXT,IntegerToString(tpPts));
double sl=basketSL; bool ta=basketTrailActive; CalcBasketAvg(); basketSL=sl; basketTrailActive=ta;
lastMsg=StringFormat("Basket TP = %d points",tpPts);
}
else lastMsg="Invalid basket TP | حد سود نامعتبر";
}
}
bool InMod(const int x,const int y)
{
return modOpen && x>=modX && x<modX+MOD_W && y>=modY && y<modY+MOD_H;
}
string HitMod(const int x,const int y)
{
if(!modOpen) return "";
if(y>=modY+72 && y<modY+98)
{
if(x>=modX+10 && x<modX+94) return "MOD_OK";
if(x>=modX+100 && x<modX+190) return "MOD_NOSL";
if(x>=modX+196 && x<modX+286) return "MOD_NOTP";
}
if(y>=modY+108 && y<modY+134 && x>=modX+196 && x<modX+286) return "MOD_X";
return "";
}
void CloseMod()
{
if(!modOpen) return;
ObjectsDeleteAll(panelChart,P+"MOD_");
modOpen=false; editing=false;
ChartRedraw(panelChart);
}
void OpenMod(const int i)
{
if(InpTradeMode!=MODE_SINGLE)
{ lastMsg="Martingale: SL/TP belong to the basket | در مارتینگل SL/TP برای کل سبد است"; uiForce=true; return; }
if(i<0 || i>=ArraySize(pos) || !pos[i].open_) return;
CloseVol();
CloseMod();
modOpen=true; modTicket=pos[i].ticket;
modX=MathMax(2,trX+40);
modY=(trY>MOD_H+12)?trY-MOD_H-6:trY+trH+6;
Rect("MOD_BG",modX,modY,MOD_W,MOD_H,C'30,36,54',C'0,150,255',3400);
RawBtn(P+"MOD_T",StringFormat("Modify #%I64u %s %.2f",pos[i].ticket,(pos[i].dir>0?"buy":"sell"),pos[i].lots),
modX+3,modY+3,MOD_W-6,24,C'0,92,170',clrWhite,C'0,92,170',9,3500);
Lbl("MOD_LS","S/L",modX+10,modY+42,clrWhite,9);
EditBox("MOD_SL",pos[i].sl>0?Px(pos[i].sl):"0",modX+40,modY+36,100,26);
Lbl("MOD_LT","T/P",modX+156,modY+42,clrWhite,9);
EditBox("MOD_TP",pos[i].tp>0?Px(pos[i].tp):"0",modX+186,modY+36,100,26);
ObjectSetInteger(panelChart,P+"MOD_SL",OBJPROP_ZORDER,3500);
ObjectSetInteger(panelChart,P+"MOD_TP",OBJPROP_ZORDER,3500);
RawBtn(P+"MOD_OK", "Apply", modX+10, modY+72,84, 26,clrForestGreen,clrWhite,Lighten(clrForestGreen,55),9,3500);
RawBtn(P+"MOD_NOSL", "Remove SL", modX+100,modY+72,90, 26,clrFireBrick, clrWhite,Lighten(clrFireBrick,55), 9,3500);
RawBtn(P+"MOD_NOTP", "Remove TP", modX+196,modY+72,90, 26,clrFireBrick, clrWhite,Lighten(clrFireBrick,55), 9,3500);
RawBtn(P+"MOD_X", "Close", modX+196,modY+108,90,26,clrDimGray, clrWhite,Lighten(clrDimGray,55), 9,3500);
Lbl("MOD_H","0 = no level | صفر یعنی بدون سطح",modX+10,modY+114,C'140,152,185',8);
ChartRedraw(panelChart);
}
void ApplyMod()
{
int i=FindPosByTicket(modTicket);
if(i<0){ CloseMod(); return; }
string t1=ObjectGetString(panelChart,P+"MOD_SL",OBJPROP_TEXT), t2=ObjectGetString(panelChart,P+"MOD_TP",OBJPROP_TEXT);
StringReplace(t1,",","."); StringReplace(t2,",",".");
string e1="", e2="";
bool a=SetPosLevel(i,true,StringToDouble(t1),e1);
bool b=SetPosLevel(i,false,StringToDouble(t2),e2);
if(a && b){ lastMsg=StringFormat("#%I64u SL/TP updated",modTicket); CloseMod(); }
else lastMsg=(a?e2:e1);
uiForce=true;
}
void ModRemove(const bool isSL)
{
int i=FindPosByTicket(modTicket);
if(i<0){ CloseMod(); return; }
string e="";
SetPosLevel(i,isSL,0,e);
ObjectSetString(panelChart,P+(isSL?"MOD_SL":"MOD_TP"),OBJPROP_TEXT,"0");
lastMsg=StringFormat("#%I64u %s removed",modTicket,(isSL?"SL":"TP"));
uiForce=true;
}
//+------------------------------------------------------------------+
//| Change Volume of an open position = PARTIAL CLOSE (MT5 style) |
//| the removed part is closed at market, its P/L goes to balance |
//+------------------------------------------------------------------+
bool PartialClose(const int i,const double newLots0,string &err)
{
err="";
if(i<0 || i>=ArraySize(pos) || !pos[i].open_){ err="Position not found | معامله پیدا نشد"; return false; }
double st=MathMax(InpLotStep,0.0001);
double nl=NormalizeDouble(MathRound(newLots0/st)*st,2);
if(newLots0<=0 || nl<InpLotMin)
{ err=StringFormat("Minimum volume is %.2f | حداقل حجم %.2f است",InpLotMin,InpLotMin); return false; }
if(nl>pos[i].lots+1e-9)
{ err=StringFormat("Volume can only be reduced (now %.2f) | حجم فقط کاهش می‌یابد",pos[i].lots); return false; }
double cl=NormalizeDouble(pos[i].lots-nl,2);
if(cl<=1e-9){ err="Same volume - nothing changed | حجم تغییری نکرد"; return false; }
double price=(pos[i].dir>0)?curBid:curAsk;
double pr=CalcProfit(pos[i].dir,pos[i].open,cl,price); // P/L of the closed part only
bal+=pr; dayProfit+=pr; weekProfit+=pr; monthProfit+=pr; closedToday+=pr;
AddHevPartial(pos[i],cl,price,pr);
pos[i].lots=nl;
if(InpTradeMode==MODE_MARTINGALE)
{
double sl=basketSL; bool ta=basketTrailActive;
CalcBasketAvg();
basketSL=sl; basketTrailActive=ta;
}
linesDirty=true; uiForce=true;
lastMsg=StringFormat("#%I64u closed %.2f lot, P/L %s | %.2f remains",pos[i].ticket,cl,MoneyS(pr),nl);
pendLog+=StringFormat("Partial close #%I64u %s %.2f @ %s P/L=%+.2f | remaining %.2f\n",
pos[i].ticket,(pos[i].dir>0?"BUY":"SELL"),cl,Px(price),pr,nl);
return true;
}
bool InVol(const int x,const int y)
{
return volOpen && x>=volX && x<volX+MOD_W && y>=volY && y<volY+MOD_H;
}
string HitVol(const int x,const int y)
{
if(!volOpen) return "";
if(y>=volY+72 && y<volY+98)
{
if(x>=volX+10 && x<volX+94) return "VOL_OK";
if(x>=volX+196 && x<volX+286) return "VOL_X";
}
return "";
}
void CloseVol()
{
if(!volOpen) return;
ObjectsDeleteAll(panelChart,P+"VOL_");
volOpen=false; editing=false;
ChartRedraw(panelChart);
}
void OpenVol(const int i)
{
if(i<0 || i>=ArraySize(pos) || !pos[i].open_) return;
CloseMod(); CloseVol();
volOpen=true; volTicket=pos[i].ticket;
volX=MathMax(2,trX+40);
volY=(trY>MOD_H+12)?trY-MOD_H-6:trY+trH+6;
Rect("VOL_BG",volX,volY,MOD_W,MOD_H,C'30,36,54',C'0,150,255',3400);
RawBtn(P+"VOL_T",StringFormat("Volume #%I64u %s %.2f",pos[i].ticket,(pos[i].dir>0?"buy":"sell"),pos[i].lots),
volX+3,volY+3,MOD_W-6,24,C'0,92,170',clrWhite,C'0,92,170',9,3500);
Lbl("VOL_LV","Volume",volX+10,volY+42,clrWhite,9);
EditBox("VOL_V",DoubleToString(pos[i].lots,2),volX+70,volY+36,100,26);
ObjectSetInteger(panelChart,P+"VOL_V",OBJPROP_ZORDER,3500);
Lbl("VOL_LC",StringFormat("max %.2f",pos[i].lots),volX+182,volY+42,C'140,152,185',9);
RawBtn(P+"VOL_OK","Apply",volX+10,volY+72,84,26,clrForestGreen,clrWhite,Lighten(clrForestGreen,55),9,3500);
RawBtn(P+"VOL_X","Close",volX+196,volY+72,90,26,clrDimGray,clrWhite,Lighten(clrDimGray,55),9,3500);
Lbl("VOL_H","New volume < current: the rest is closed at market | حجم کمتر یعنی بستن بخشی از معامله",volX+10,volY+108,C'140,152,185',7);
ChartRedraw(panelChart);
}
void ApplyVol()
{
int i=FindPosByTicket(volTicket);
if(i<0){ CloseVol(); return; }
string t=ObjectGetString(panelChart,P+"VOL_V",OBJPROP_TEXT);
StringReplace(t,",","."); StringTrimLeft(t); StringTrimRight(t);
string e="";
if(PartialClose(i,StringToDouble(t),e)) CloseVol();
else { lastMsg=e; uiForce=true; }
}
void ApplyTrailText()
{
long a=StringToInteger(ObjectGetString(panelChart,P+"TRS",OBJPROP_TEXT));
long b=StringToInteger(ObjectGetString(panelChart,P+"TRP",OBJPROP_TEXT));
if(a<0) a=0; if(b<0) b=0;
trStart=(int)a; trStep=(int)b;
ObjectSetString(panelChart,P+"TRS",OBJPROP_TEXT,IntegerToString(trStart));
ObjectSetString(panelChart,P+"TRP",OBJPROP_TEXT,IntegerToString(trStep));
lastMsg=StringFormat("Trailing: start %d step %d points",trStart,trStep);
uiForce=true;
}
void ToggleTrail()
{
trailOn=!trailOn;
if(!trailOn && InpTradeMode==MODE_MARTINGALE){ basketTrailActive=false; basketSL=0; linesDirty=true; }
lastMsg=trailOn?"Trailing stop ON | تریلینگ روشن":"Trailing stop OFF | تریلینگ خاموش";
btnDirty=true; uiForce=true;
}
void EndDrags()
{
if(dragKind!=0) ApplyDrag();
if(trSize) trSizeDirty=true;
sliderDrag=false; trDrag=false; trSize=false;
ChartSetInteger(panelChart,CHART_MOUSE_SCROLL,true);
}
//--- press dispatcher -------------------------------------------------
void PressAt(const int x,const int y)
{
if(ctxOpen)
{
string c=HitCtx(x,y);
CloseCtx();
if(c!=""){ MarkPressed(c); Act(c); return; }
}
if(calOpen)
{
if(InCal(x,y))
{
string c=HitCal(x,y);
if(c!=""){ MarkPressed(c); Act(c); }
return;
}
string pid=HitPanel(x,y);
bool same=(calTarget==0 && (pid=="FROM"||pid=="FROM_I")) || (calTarget==1 && (pid=="TO"||pid=="TO_I"));
CloseCal();
if(same){ MarkPressed(pid); return; }
if(pid!=""){ MarkPressed(pid); Act(pid); }
return;
}
if(volOpen)
{
if(InVol(x,y)){ string vc=HitVol(x,y); if(vc!=""){ MarkPressed(vc); Act(vc); } return; }
CloseVol();
}
if(modOpen)
{
if(InMod(x,y)){ string mc=HitMod(x,y); if(mc!=""){ MarkPressed(mc); Act(mc); } return; }
CloseMod();
}
if(InTrades(x,y)){ HandleTrades(x,y); return; }
if(x>=slX-6 && x<slX+slW+6 && y>=slY-4 && y<slY+SL_H+4)
{
sliderDrag=true;
ChartSetInteger(panelChart,CHART_MOUSE_SCROLL,false);
SliderAt(x);
return;
}
if(!(x>=4 && x<4+panW && y>=4 && y<4+panH) && TryStartLineDrag(x,y)) return;
if(!InpFireOnPress) return;
string id=HitPanel(x,y);
if(id!=""){ MarkPressed(id); Act(id); }
}
void SetPlay(const bool p)
{
playing=p;
lastUs=GetMicrosecondCount();
EventSetMillisecondTimer(p?1:10);
btnDirty=true; uiForce=true;
}
// Play applies the chosen date range first (if it changed or replay finished)
void StartPlay()
{
bool atEnd=(cur>=endBar && tkIdx>=tkN && barClockUs>=barDurUs);
if(selDirty || atEnd) ApplyRange();
SetPlay(true);
}
void Act(const string b)
{
if(b=="BUY") OpenTrade(1);
else if(b=="SELL") OpenTrade(-1);
else if(b=="CLOSE") CloseAll();
else if(b=="PLAY") { StartPlay(); UpdateButtonStates(true); btnDirty=false; }
else if(b=="PAUSE") { SetPlay(false); UpdateButtonStates(true); btnDirty=false; }
else if(b=="LIVE") { SetSpeed(1.0); UpdateButtonStates(true); btnDirty=false; }
else if(b=="LOT_M") SetLot(lotSize-InpLotStep);
else if(b=="LOT_P") SetLot(lotSize+InpLotStep);
else if(b=="MAXLOT") SetLot(MaxLotFor(FreeMargin(),curAsk));
else if(b=="APPLY") ApplySLTPOpen();
else if(b=="BAL_RESET") ResetBalance();
else if(b=="BAL_DEP") Deposit();
else if(b=="TRAIL_T") ToggleTrail();
else if(b=="MOD_OK") ApplyMod();
else if(b=="MOD_NOSL") ModRemove(true);
else if(b=="MOD_NOTP") ModRemove(false);
else if(b=="MOD_X") CloseMod();
else if(b=="VOL_OK") ApplyVol();
else if(b=="VOL_X") CloseVol();
else if(b=="TRADES"){ if(trOpen) CloseTrades(); else OpenTrades(); UpdateButtonStates(true); btnDirty=false; }
else if(b=="TW_CLOSE"){ CloseTrades(); UpdateButtonStates(true); btnDirty=false; }
else if(b=="CTX_BL") { PlaceOrder(1,ctxPrice); CloseCtx(); }
else if(b=="CTX_SLM") { PlaceOrder(2,ctxPrice); CloseCtx(); }
else if(b=="CTX_BS") { PlaceOrder(3,ctxPrice); CloseCtx(); }
else if(b=="CTX_SS") { PlaceOrder(4,ctxPrice); CloseCtx(); }
else if(b=="TF_M1") SwitchTF(PERIOD_M1);
else if(b=="TF_M5") SwitchTF(PERIOD_M5);
else if(b=="TF_M15") SwitchTF(PERIOD_M15);
else if(b=="TF_M30") SwitchTF(PERIOD_M30);
else if(b=="TF_H1") SwitchTF(PERIOD_H1);
else if(b=="TF_H4") SwitchTF(PERIOD_H4);
else if(b=="TF_D1") SwitchTF(PERIOD_D1);
else if(b=="TF_W1") SwitchTF(PERIOD_W1);
else if(b=="FROM"||b=="FROM_I") { if(calOpen && calTarget==0) CloseCal(); else OpenCal(0); }
else if(b=="TO"||b=="TO_I") { if(calOpen && calTarget==1) CloseCal(); else OpenCal(1); }
else if(b=="CAL_PREV"){ calMon--; if(calMon<1){calMon=12;calYear--;} RefreshCal(); ChartRedraw(panelChart); }
else if(b=="CAL_NEXT"){ calMon++; if(calMon>12){calMon=1;calYear++;} RefreshCal(); ChartRedraw(panelChart); }
else if(b=="CAL_TODAY"){ PickDate(MathMax(dataFirst,MathMin(dataLast,DayStart(TimeLocal())))); }
else if(StringFind(b,"CAL_D")==0)
{
int i=(int)StringToInteger(StringSubstr(b,5));
if(i>=0 && i<42 && calOn[i]) PickDate(calDate[i]);
}
uiForce=true;
}
void OnChartEvent(const int id,const long &lp,const double &dp,const string &sp)
{
if(!ready) return;
if(id==CHARTEVENT_MOUSE_MOVE)
{
int x=(int)lp, y=(int)dp;
uint fl=(uint)StringToInteger(sp);
bool ld=((fl&1)!=0), rd=((fl&2)!=0);
if(ld)
{
if(sliderDrag) SliderAt(x);
else if(trDrag){ trX=MathMax(0,x-trDX); trY=MathMax(0,y-trDY); MoveTrades(); ChartRedraw(panelChart); }
else if(trSize){ trW=MathMax(600,x-trX+10); trH=MathMax(160,y-trY+10); trSizeDirty=true; }
else if(dragKind!=0) DragMove(y);
}
else if(sliderDrag||trDrag||trSize||dragKind!=0) EndDrags();
if(ld && !mouseDown) PressAt(x,y);
if(rd && !rightDown) RightClickAt(x,y);
mouseDown=ld; rightDown=rd;
return;
}
if(id==CHARTEVENT_OBJECT_ENDEDIT)
{
editing=false;
if(sp==P+"LOT") ApplyLotText();
else if(sp==P+"LEV") ApplyLevText();
else if(sp==P+"SLP"||sp==P+"TPP") ApplySLTPText();
else if(sp==P+"BALV") ApplyBalText();
else if(sp==P+"TRS"||sp==P+"TRP") ApplyTrailText();
return;
}
// hotkeys: B = Buy, S = Sell, C = Close all, Space = Play/Pause, +/- = speed, Esc = close popups
if(id==CHARTEVENT_KEYDOWN)
{
if(editing) return;
if(lp==66) OpenTrade(1);
else if(lp==83) OpenTrade(-1);
else if(lp==67) CloseAll();
else if(lp==32) { if(playing) Act("PAUSE"); else Act("PLAY"); }
else if(lp==107||lp==187) SpeedStep(1);
else if(lp==109||lp==189) SpeedStep(-1);
else if(lp==27) { CloseCtx(); CloseCal(); CloseMod(); CloseVol(); }
return;
}
// fallback path (click on release) - skipped when the press already fired
if(id!=CHARTEVENT_OBJECT_CLICK) return;
if(StringFind(sp,P)!=0) return;
if(IsEditName(sp)){ editing=true; return; }
string b=StringSubstr(sp,StringLen(P));
if(b=="") return;
bool dup=(b==pressId && (GetTickCount64()-pressMs)<1500);
if(dup) pressId=""; else Act(b);
ObjectSetInteger(panelChart,sp,OBJPROP_STATE,false);
btnDirty=true; uiForce=true;
}
//+------------------------------------------------------------------+
//| TF switch: terminal rebuilds every TF from ticks, so no reload. |
//+------------------------------------------------------------------+
void SwitchTF(ENUM_TIMEFRAMES tf)
{
if(tf==currentTF) return;
FlushTicks();
ChartSetSymbolPeriod(panelChart,customSym,tf);
}
datetime DayStart(const datetime t){ return (datetime)(((long)t/86400)*86400); }
int FindFirst(const datetime t) // first bar with time >= t
{
int lo=0,hi=total-1;
while(lo<hi){ int mid=(lo+hi)/2; if(rates[mid].time<t) lo=mid+1; else hi=mid; }
return lo;
}
int FindLast(const datetime t) // last bar with time <= t
{
int lo=0,hi=total-1;
while(lo<hi){ int mid=(lo+hi+1)/2; if(rates[mid].time>t) hi=mid-1; else lo=mid; }
return lo;
}
void ApplyRange()
{
if(selTo<selFrom) selTo=selFrom;
int bf=FindFirst(selFrom);
int bt=FindLast(selTo+86399);
if(bt<=bf) bt=total-1;
SetPlay(false);
endBar=bt;
selDirty=false;
ResetTradeState(false);
ClearAllLines();
StartAt(bf);
linesDirty=true;
}
void ResetTradeState(const bool resetStats)
{
ArrayResize(pos,0); openCnt=0; needCompact=false;
ArrayResize(ords,0); ordLive=0; ordCompact=false;
martLevel=0; martDir=0;
basketAvg=0; basketLots=0; basketSL=0; basketTP=0; bDir=0; basketTrailActive=false;
if(resetStats)
{
nextT=1; lastDay=0; lastWeek=0; lastMonth=0;
dayProfit=0; ydayProfit=0; weekProfit=0; monthProfit=0;
dayLots=0; weekLots=0; monthLots=0; closedToday=0;
}
}
//+------------------------------------------------------------------+
//| Replay positioning |
//| History = base bars [from..idx-1] + coarser TFs stitched in |
//| for everything older (complete higher-TF history, no holes) |
//+------------------------------------------------------------------+
void PushHistory(const int idx)
{
int from=MathMax(0,idx-InpHistoryBars);
CustomRatesDelete(customSym,0,LONG_MAX);
CustomTicksDelete(customSym,0,LONG_MAX);
datetime boundary=rates[from].time;
int segS[8], segC[8], segN=0;
int totalN=idx-from;
for(int k=0;k<cK && segN<8;k++)
{
int ps=PeriodSeconds(cTF[k]);
int off=cOff[k], m=cCnt[k];
int lo=0, hi=m;
while(lo<hi){ int mid=(lo+hi)/2; if(cser[off+mid].time+ps<=boundary) lo=mid+1; else hi=mid; }
if(lo<=0) continue;
segS[segN]=off; segC[segN]=lo; segN++;
totalN+=lo;
boundary=cser[off].time;
}
if(totalN<=0) return;
MqlRates all[];
ArrayResize(all,totalN);
int w=0;
for(int s=segN-1;s>=0;s--){ ArrayCopy(all,cser,w,segS[s],segC[s]); w+=segC[s]; }
if(idx-from>0) ArrayCopy(all,rates,w,from,idx-from);
CustomRatesUpdate(customSym,all);
}
void StartAt(const int idx)
{
cur=idx;
PushHistory(idx);
curBid=(idx>0)?rates[idx-1].close:rates[0].open;
curAsk=curBid+spreadPx;
UpdateStatsPeriod(rates[cur].time);
BuildBarTicks(cur);
tkIdx=0; barClockUs=0;
lastUs=GetMicrosecondCount();
Advance(0.0); // emit opening tick so the first candle is visible
}
void UpdateStatsPeriod(const datetime t)
{
datetime dayStart=(datetime)(((long)t/86400)*86400);
if(dayStart==lastDay && lastWeek!=0 && lastMonth!=0) return;
MqlDateTime d; TimeToStruct(dayStart,d);
long dn=(long)dayStart/86400;
datetime weekStart =(datetime)((dn-((dn+3)%7))*86400); // Monday based
datetime monthStart=(datetime)(((long)dayStart)-(long)(d.day-1)*86400);
if(lastDay==0) lastDay=dayStart;
else if(dayStart!=lastDay){ ydayProfit=dayProfit; dayProfit=0; dayLots=0; closedToday=0; lastDay=dayStart; }
if(lastWeek==0) lastWeek=weekStart;
else if(weekStart!=lastWeek){ weekProfit=0; weekLots=0; lastWeek=weekStart; }
if(lastMonth==0) lastMonth=monthStart;
else if(monthStart!=lastMonth){ monthProfit=0; monthLots=0; lastMonth=monthStart; }
}
//+------------------------------------------------------------------+
//| Data: full multi-TF export / cached load |
//+------------------------------------------------------------------+
string CsvPath(ENUM_TIMEFRAMES tf){ return folderName+"\\"+baseSym+"+"+TFName(tf)+".csv"; }
string BinPath(ENUM_TIMEFRAMES tf){ return folderName+"\\"+baseSym+"+"+TFName(tf)+".bin"; }
int TfCandidates(ENUM_TIMEFRAMES &c[])
{
ArrayResize(c,8);
c[0]=PERIOD_M1; c[1]=PERIOD_M5; c[2]=PERIOD_M15; c[3]=PERIOD_M30; c[4]=PERIOD_H1; c[5]=PERIOD_H4; c[6]=PERIOD_D1; c[7]=PERIOD_W1;
int s=0;
for(int i=0;i<8;i++) if(c[i]==InpBaseTF){ s=i; break; }
return s;
}
// make sure the terminal really has the whole history before exporting
int FetchFull(const string sym,ENUM_TIMEFRAMES tf,MqlRates &r[])
{
for(int w=0;w<25;w++){ if((bool)SeriesInfoInteger(sym,tf,SERIES_SYNCHRONIZED)) break; Sleep(200); }
int prev=-1, n=0;
for(int k=0;k<8;k++)
{
n=CopyRates(sym,tf,0,InpBarsToSave,r);
if(n<=0){ Sleep(250); continue; }
if(n==prev || n>=InpBarsToSave) break; // stopped growing => fully loaded
prev=n; Sleep(250);
}
return n;
}
void SaveSource()
{
ENUM_TIMEFRAMES c[]; int s=TfCandidates(c);
SymbolSelect(_Symbol,true);
for(int k=s;k<8;k++)
{
if(!InpResaveOnInit && FileIsExist(CsvPath(c[k]))) continue;
MqlRates r[];
int n=FetchFull(_Symbol,c[k],r);
if(n<=0){ Print("No history for ",TFName(c[k])," on ",_Symbol); continue; }
if(r[n-1].time+PeriodSeconds(c[k])>TimeCurrent()) n--; // drop the still-forming bar
if(n<=0) continue;
int h=FileOpen(CsvPath(c[k]),FILE_WRITE|FILE_CSV|FILE_ANSI,',');
if(h==INVALID_HANDLE){ Print("Cannot write ",CsvPath(c[k])); continue; }
FileWrite(h,"DateTime","Open","High","Low","Close","TickVolume","Spread","RealVolume");
for(int j=0;j<n;j++)
FileWrite(h,TimeToString(r[j].time,TIME_DATE|TIME_SECONDS),
DoubleToString(r[j].open,_Digits),DoubleToString(r[j].high,_Digits),
DoubleToString(r[j].low,_Digits),DoubleToString(r[j].close,_Digits),
(long)r[j].tick_volume,(int)r[j].spread,(long)r[j].real_volume);
FileClose(h);
FileDelete(BinPath(c[k])); // invalidate binary cache
Print("Saved ",TFName(c[k])," | bars: ",n," | ",TimeToString(r[0].time)," -> ",TimeToString(r[n-1].time));
}
}
bool SetupCustom()
{
if(!SymbolInfoInteger(customSym,SYMBOL_EXIST))
{
if(!CustomSymbolCreate(customSym,"Custom\\Replay")) return false;
}
CustomSymbolSetInteger(customSym,SYMBOL_DIGITS,digitsN);
CustomSymbolSetDouble(customSym,SYMBOL_POINT,InpPoint);
CustomSymbolSetDouble(customSym,SYMBOL_TRADE_TICK_SIZE,InpPoint);
CustomSymbolSetDouble(customSym,SYMBOL_TRADE_CONTRACT_SIZE,InpContractSize);
CustomSymbolSetInteger(customSym,SYMBOL_TRADE_CALC_MODE,SYMBOL_CALC_MODE_CFD);
CustomSymbolSetInteger(customSym,SYMBOL_TRADE_MODE,SYMBOL_TRADE_MODE_FULL);
SymbolSelect(customSym,true); // required for CustomTicksAdd
return true;
}
// binary cache: loads hundreds of thousands of bars in milliseconds
bool LoadBin(ENUM_TIMEFRAMES tf,MqlRates &out[])
{
if(!FileIsExist(BinPath(tf)) || !FileIsExist(CsvPath(tf))) return false;
int hc=FileOpen(CsvPath(tf),FILE_READ|FILE_TXT|FILE_ANSI|FILE_SHARE_READ);
if(hc==INVALID_HANDLE) return false;
long dCsv=FileGetInteger(hc,FILE_MODIFY_DATE); FileClose(hc);
int h=FileOpen(BinPath(tf),FILE_READ|FILE_BIN);
if(h==INVALID_HANDLE) return false;
bool ok=false;
if(FileGetInteger(h,FILE_MODIFY_DATE)>=dCsv && FileReadInteger(h)==0x42494E32 && FileReadInteger(h)==InpBarsToLoad)
{
int n=FileReadInteger(h);
if(n>=10 && n<=20000000)
{
ArrayResize(out,n);
ok=((int)FileReadArray(h,out,0,n)==n);
}
}
FileClose(h);
return ok;
}
void SaveBin(ENUM_TIMEFRAMES tf,MqlRates &src[])
{
int n=ArraySize(src);
int h=FileOpen(BinPath(tf),FILE_WRITE|FILE_BIN);
if(h==INVALID_HANDLE) return;
FileWriteInteger(h,0x42494E32); FileWriteInteger(h,InpBarsToLoad); FileWriteInteger(h,n);
FileWriteArray(h,src,0,n);
FileClose(h);
}
bool ReadSeries(ENUM_TIMEFRAMES tf,MqlRates &out[])
{
if(LoadBin(tf,out)) return ArraySize(out)>=10;
if(!FileIsExist(CsvPath(tf)))
{
int n0=CopyRates(baseSym,tf,0,InpBarsToLoad,out);
return n0>=10;
}
int h=FileOpen(CsvPath(tf),FILE_READ|FILE_TXT|FILE_ANSI|FILE_SHARE_READ);
if(h==INVALID_HANDLE) return false;
MqlRates tmp[];
ArrayResize(tmp,0,MathMin(InpBarsToSave,3000000)+16);
int n=0; string line; string p[]; datetime lastT=0;
while(!FileIsEnding(h))
{
line=FileReadString(h);
if(StringLen(line)<16) continue;
int k=StringSplit(line,',',p);
if(k<6) continue;
datetime t=StringToTime(p[0]);
if(t<=0 || t<=lastT) continue; // header / duplicates / disorder
double o=StringToDouble(p[1]), hi=StringToDouble(p[2]), lo=StringToDouble(p[3]), cl=StringToDouble(p[4]);
if(o<=0 || hi<=0 || lo<=0 || cl<=0) continue;
MqlRates rr; ZeroMemory(rr);
rr.time=t; rr.open=o; rr.high=hi; rr.low=lo; rr.close=cl;
rr.tick_volume=(long)StringToInteger(p[4+1]); if(rr.tick_volume<=0) rr.tick_volume=1;
if(k>=7) rr.spread=(int)StringToInteger(p[6]);
if(k>=8) rr.real_volume=(long)StringToInteger(p[7]);
ArrayResize(tmp,n+1);
tmp[n++]=rr; lastT=t;
}
FileClose(h);
if(n<10) return false;
int keep=MathMin(n,InpBarsToLoad); // newest bars win
ArrayResize(out,keep);
ArrayCopy(out,tmp,0,n-keep,keep);
SaveBin(tf,out);
return true;
}
bool LoadBase()
{
ENUM_TIMEFRAMES c[]; int s=TfCandidates(c);
int kb=-1;
for(int k=s;k<8;k++) if(ReadSeries(c[k],rates)){ kb=k; break; }
if(kb<0) return false;
total=ArraySize(rates);
usedTF=c[kb];
ArrayResize(cser,0); cK=0;
int w=0;
for(int k=kb+1;k<8 && cK<8;k++)
{
MqlRates t[];
if(!ReadSeries(c[k],t)) continue;
int m=ArraySize(t);
ArrayResize(cser,w+m);
ArrayCopy(cser,t,w,0,m);
cTF[cK]=c[k]; cOff[cK]=w; cCnt[cK]=m; cK++; w+=m;
}
Print("Loaded base ",TFName(usedTF)," bars: ",total," | ",TimeToString(rates[0].time)," -> ",TimeToString(rates[total-1].time),
" | stitched coarser TFs: ",cK);
return total>=10;
}
//+------------------------------------------------------------------+
//| Persist / restore state across TF changes (MT5 re-inits the EA) |
//+------------------------------------------------------------------+
void SaveState()
{
int h=FileOpen(StatePath(),FILE_WRITE|FILE_BIN);
if(h==INVALID_HANDLE) return;
FileWriteInteger(h,0x52504C5C);
FileWriteLong(h,(long)TimeLocal());
FileWriteInteger(h,cur); FileWriteInteger(h,endBar); FileWriteInteger(h,tkIdx);
FileWriteDouble(h,barClockUs); FileWriteDouble(h,bal); FileWriteDouble(h,speedMult); FileWriteDouble(h,lotSize);
FileWriteInteger(h,leverage); FileWriteInteger(h,slPts); FileWriteInteger(h,tpPts); FileWriteDouble(h,resetBal);
FileWriteInteger(h,trailOn?1:0); FileWriteInteger(h,trStart); FileWriteInteger(h,trStep);
FileWriteInteger(h,playing?1:0); FileWriteLong(h,(long)nextT);
FileWriteInteger(h,martLevel); FileWriteInteger(h,martDir);
FileWriteDouble(h,basketSL); FileWriteInteger(h,basketTrailActive?1:0);
FileWriteDouble(h,curBid);
FileWriteDouble(h,dayProfit); FileWriteDouble(h,ydayProfit); FileWriteDouble(h,weekProfit); FileWriteDouble(h,monthProfit);
FileWriteDouble(h,dayLots); FileWriteDouble(h,weekLots); FileWriteDouble(h,monthLots); FileWriteDouble(h,closedToday);
FileWriteLong(h,(long)lastDay); FileWriteLong(h,(long)lastWeek); FileWriteLong(h,(long)lastMonth);
FileWriteLong(h,(long)selFrom); FileWriteLong(h,(long)selTo); FileWriteInteger(h,selDirty?1:0);
FileWriteInteger(h,trOpen?1:0); FileWriteInteger(h,trX); FileWriteInteger(h,trY); FileWriteInteger(h,trW); FileWriteInteger(h,trH);
int n=ArraySize(pos);
FileWriteInteger(h,n);
for(int i=0;i<n;i++) FileWriteStruct(h,pos[i]);
int m=ArraySize(ords);
FileWriteInteger(h,m);
for(int i=0;i<m;i++) FileWriteStruct(h,ords[i]);
FileClose(h);
}
bool TryRestoreState()
{
if(!FileIsExist(StatePath())) return false;
int h=FileOpen(StatePath(),FILE_READ|FILE_BIN);
if(h==INVALID_HANDLE) return false;
bool ok=false;
if(FileReadInteger(h)==0x52504C5C)
{
long stamp=FileReadLong(h);
if(MathAbs((long)TimeLocal()-stamp)<=30)
{
cur=FileReadInteger(h); endBar=FileReadInteger(h); tkIdx=FileReadInteger(h);
barClockUs=FileReadDouble(h); bal=FileReadDouble(h); speedMult=FileReadDouble(h); lotSize=FileReadDouble(h);
leverage=FileReadInteger(h); slPts=FileReadInteger(h); tpPts=FileReadInteger(h); resetBal=FileReadDouble(h);
trailOn=(FileReadInteger(h)==1); trStart=FileReadInteger(h); trStep=FileReadInteger(h);
playing=(FileReadInteger(h)==1); nextT=(ulong)FileReadLong(h);
martLevel=FileReadInteger(h); martDir=FileReadInteger(h);
basketSL=FileReadDouble(h); basketTrailActive=(FileReadInteger(h)==1);
curBid=FileReadDouble(h);
dayProfit=FileReadDouble(h); ydayProfit=FileReadDouble(h); weekProfit=FileReadDouble(h); monthProfit=FileReadDouble(h);
dayLots=FileReadDouble(h); weekLots=FileReadDouble(h); monthLots=FileReadDouble(h); closedToday=FileReadDouble(h);
lastDay=(datetime)FileReadLong(h); lastWeek=(datetime)FileReadLong(h); lastMonth=(datetime)FileReadLong(h);
selFrom=(datetime)FileReadLong(h); selTo=(datetime)FileReadLong(h); selDirty=(FileReadInteger(h)==1);
trOpen=(FileReadInteger(h)==1); trX=FileReadInteger(h); trY=FileReadInteger(h); trW=FileReadInteger(h); trH=FileReadInteger(h);
int n=FileReadInteger(h);
ArrayResize(pos,0); ArrayResize(ords,0);
if(n>=0 && n<100000)
{
ArrayResize(pos,n,64);
for(int i=0;i<n;i++) FileReadStruct(h,pos[i]);
int m=FileReadInteger(h);
if(m>=0 && m<100000)
{
ArrayResize(ords,m,16);
for(int i=0;i<m;i++) FileReadStruct(h,ords[i]);
ok=(cur>=0 && cur<total && endBar<total);
}
}
}
}
FileClose(h);
FileDelete(StatePath());
if(!ok){ ArrayResize(pos,0); ArrayResize(ords,0); return false; }
if(speedMult<SPD_MIN) speedMult=1.0;
if(lotSize<=0) lotSize=InpLot;
if(leverage<1) leverage=InpLeverage;
if(resetBal<=0) resetBal=InpBalance;
if(trStart<0) trStart=0; if(trStep<0) trStep=0;
endBar=MathMin(endBar,total-1);
curAsk=curBid+spreadPx;
openCnt=0; ordLive=0;
for(int i=0;i<ArraySize(pos);i++) if(pos[i].open_) openCnt++;
for(int i=0;i<ArraySize(ords);i++) if(ords[i].live) ordLive++;
if(InpTradeMode==MODE_MARTINGALE){ double sl=basketSL; bool ta=basketTrailActive; CalcBasketAvg(); basketSL=sl; basketTrailActive=ta; }
BuildBarTicks(cur);
if(tkIdx>tkN) tkIdx=tkN;
eq=bal;
Print("State restored after TF change | وضعیت حفظ شد. Bar=",cur);
return true;
}
//+------------------------------------------------------------------+
//| Chart lines - MetaTrader 5 standard look |
//| dotted lines, labels "#ticket buy 0.10" / "#ticket sl|tp" |
//+------------------------------------------------------------------+
void DrawHLine(const string name,const double price,const color clr,const ENUM_LINE_STYLE st,const int w,const string txt)
{
if(dragKind!=0 && name==dragName) return; // do not fight the user while a line is being dragged
if(ObjectFind(panelChart,name)<0)
{
ObjectCreate(panelChart,name,OBJ_HLINE,0,0,price);
ObjectSetInteger(panelChart,name,OBJPROP_COLOR,clr);
ObjectSetInteger(panelChart,name,OBJPROP_STYLE,st);
ObjectSetInteger(panelChart,name,OBJPROP_WIDTH,w);
ObjectSetInteger(panelChart,name,OBJPROP_BACK,true);
ObjectSetInteger(panelChart,name,OBJPROP_SELECTABLE,false);
}
else ObjectSetDouble(panelChart,name,OBJPROP_PRICE,price);
ObjectSetString(panelChart,name,OBJPROP_TEXT,txt);
}
void DeletePosLines(const ulong ticket)
{
string t=IntegerToString((long)ticket);
ObjectDelete(panelChart,P+"E_"+t);
ObjectDelete(panelChart,P+"S_"+t);
ObjectDelete(panelChart,P+"T_"+t);
}
void ClearBasketLines()
{
ObjectDelete(panelChart,P+"BAVG");
ObjectDelete(panelChart,P+"BTP");
ObjectDelete(panelChart,P+"BSL");
}
void ClearAllLines()
{
ObjectsDeleteAll(panelChart,P+"E_");
ObjectsDeleteAll(panelChart,P+"S_");
ObjectsDeleteAll(panelChart,P+"T_");
ObjectsDeleteAll(panelChart,P+"O_");
ObjectsDeleteAll(panelChart,"RPH_");
ArrayResize(hev,0); ArrayResize(hDrawn,0);
ClearBasketLines();
}
void CompactPositions()
{
int w=0, n=ArraySize(pos);
for(int i=0;i<n;i++)
{
if(!pos[i].open_){ DeletePosLines(pos[i].ticket); continue; }
if(w!=i) pos[w]=pos[i];
w++;
}
ArrayResize(pos,w);
needCompact=false;
}
void CompactOrders()
{
int w=0, n=ArraySize(ords);
for(int i=0;i<n;i++)
{
if(!ords[i].live){ ObjectDelete(panelChart,P+"O_"+IntegerToString((long)ords[i].ticket)); continue; }
if(w!=i) ords[w]=ords[i];
w++;
}
ArrayResize(ords,w);
ordCompact=false;
}
void UpdateOrderLines()
{
for(int i=0;i<ArraySize(ords);i++)
{
if(!ords[i].live) continue;
string t=IntegerToString((long)ords[i].ticket);
DrawHLine(P+"O_"+t,ords[i].price,InpColEntry,STYLE_DOT,1,"#"+t+" "+OrdTypeName(ords[i].type)+" "+DoubleToString(ords[i].lots,2));
}
}
void UpdateTradeLines()
{
for(int i=0;i<ArraySize(pos);i++)
{
if(!pos[i].open_) continue;
string t=IntegerToString((long)pos[i].ticket);
DrawHLine(P+"E_"+t,pos[i].open,InpColEntry,STYLE_DOT,1,"#"+t+" "+(pos[i].dir>0?"buy ":"sell ")+DoubleToString(pos[i].lots,2));
if(InpTradeMode!=MODE_SINGLE) continue;
if(pos[i].sl>0) DrawHLine(P+"S_"+t,pos[i].sl,InpColSL,STYLE_DOT,1,"#"+t+" sl");
else ObjectDelete(panelChart,P+"S_"+t);
if(pos[i].tp>0) DrawHLine(P+"T_"+t,pos[i].tp,InpColTP,STYLE_DOT,1,"#"+t+" tp");
else ObjectDelete(panelChart,P+"T_"+t);
}
}
void UpdateBasketLines()
{
if(basketLots<=0 || basketAvg<=0){ ClearBasketLines(); return; }
DrawHLine(P+"BAVG",basketAvg,InpColEntry,STYLE_SOLID,2,StringFormat("basket %s %.2f",(bDir>0?"buy":"sell"),basketLots));
DrawHLine(P+"BTP",basketTP,InpColTP,STYLE_SOLID,2,"basket tp");
if(basketTrailActive && basketSL>0) DrawHLine(P+"BSL",basketSL,InpColSL,STYLE_SOLID,2,"basket sl");
else ObjectDelete(panelChart,P+"BSL");
}
//+------------------------------------------------------------------+
//| UI refresh (throttled, never inside the tick loop) |
//+------------------------------------------------------------------+
void SetT(const int k,const string id,const string t0)
{
string t=(t0=="")?" ":t0;
if(lcT[k]==t) return;
lcT[k]=t;
ObjectSetString(panelChart,P+id,OBJPROP_TEXT,t);
}
void SetCol(const int k,const string id,const color c)
{
if(lcC[k]==c) return;
lcC[k]=c;
ObjectSetInteger(panelChart,P+id,OBJPROP_COLOR,c);
}
void UpdateUI()
{
uiForce=false;
double flo=0, used=0, buyL=0, sellL=0; int nb=0, ns=0;
for(int i=0;i<ArraySize(pos);i++)
{
if(!pos[i].open_) continue;
double fl=CalcProfit(pos[i].dir,pos[i].open,pos[i].lots,pos[i].dir>0?curBid:curAsk);
flo+=fl;
used+=pos[i].lots*InpContractSize*pos[i].open/leverage;
if(pos[i].dir>0){ buyL+=pos[i].lots; nb++; } else { sellL+=pos[i].lots; ns++; }
ObjectSetString(panelChart,P+"E_"+IntegerToString((long)pos[i].ticket),OBJPROP_TEXT,
StringFormat("#%I64u %s %.2f %s",pos[i].ticket,(pos[i].dir>0?"buy":"sell"),pos[i].lots,MoneyS(fl)));
}
eq=bal+flo;
double freeM=eq-used;
double mlvl=(used>0)?eq/used*100.0:0;
if(needCompact) CompactPositions();
if(ordCompact) CompactOrders();
if(linesDirty)
{
linesDirty=false;
UpdateOrderLines();
UpdateTradeLines();
if(InpTradeMode==MODE_MARTINGALE) UpdateBasketLines();
}
ProcessHistory();
if(btnDirty){ btnDirty=false; UpdateButtonStates(false); }
if(pendLog!=""){ Print(pendLog); pendLog=""; }
// simulated time & candle countdown
double simSec=(double)rates[cur].time+barClockUs/1e6;
int tfs=PeriodSeconds(currentTF); if(tfs<=0) tfs=300;
double tfOff=(currentTF==PERIOD_W1)?3.0*86400.0:0.0;
double remReal=(tfs-MathMod(simSec-tfOff,(double)tfs))/MathMax(0.01,speedMult);
int rm=(int)(remReal/60.0), rs=(int)MathMod(remReal,60.0), rt=(int)(MathMod(remReal,1.0)*10.0);
// --- big quote buttons
SetT(K_SELL,"SELL","SELL "+Px(curBid));
SetT(K_BUY, "BUY", "BUY "+Px(curAsk));
SetT(K_SPEED,"SPDV",SpeedTxt(speedMult));
// --- account cells
SetT(K_BAL,"VB",Money(bal));
SetT(K_EQ, "VE",Money(eq)); SetCol(K_EQ,"VE",PnlCol(eq-bal));
SetT(K_FLO,"VF",MoneyS(flo)); SetCol(K_FLO,"VF",PnlCol(flo));
SetT(K_MG,"VMG",Money(used));
SetT(K_FM,"VFM",Money(freeM)); SetCol(K_FM,"VFM",(freeM>=0)?clrWhite:C'255,100,100');
SetT(K_ML,"VML",(used>0)?DoubleToString(mlvl,1)+"%":"--");
SetCol(K_ML,"VML",(used<=0)?clrWhite:(mlvl<InpStopOutPct*1.5)?C'255,100,100':(mlvl<150.0)?C'255,190,60':C'90,225,130');
double mpl=InpContractSize*curAsk/leverage; // margin for 1.00 lot
int fit=(mpl*lotSize>0)?(int)MathFloor(MathMax(0.0,freeM)/(mpl*lotSize)):0;
SetT(K_LI,"LI",StringFormat("Margin/1 lot %s | This lot %s | Max lot %.2f | Fits %d trade(s)",
Money(mpl),Money(mpl*lotSize),MaxLotFor(freeM,curAsk),fit));
int op=nb+ns;
SetT(K_POS,"POS",(op==0)?StringFormat("No open positions | Pending: %d",ordLive)
:StringFormat("Open %d | Buy %.2f (%d) Sell %.2f (%d) Net %+.2f | Pending: %d",op,buyL,nb,sellL,ns,buyL-sellL,ordLive));
SetT(K_TD,"VT",MoneyS(dayProfit)); SetCol(K_TD,"VT",PnlCol(dayProfit));
SetT(K_YD,"VY",MoneyS(ydayProfit)); SetCol(K_YD,"VY",PnlCol(ydayProfit));
SetT(K_WK,"VW",MoneyS(weekProfit)); SetCol(K_WK,"VW",PnlCol(weekProfit));
SetT(K_MO,"VM",MoneyS(monthProfit)); SetCol(K_MO,"VM",PnlCol(monthProfit));
SetT(K_LOTS,"LOTS",StringFormat("Lots traded Today %.2f | Week %.2f | Month %.2f",dayLots,weekLots,monthLots));
if(InpTradeMode==MODE_SINGLE)
SetT(K_MODE,"MODE",StringFormat("Mode: Single | تک معامله SL %d TP %d Trail %s %d/%d Lev 1:%d",slPts,tpPts,(trailOn?"ON":"OFF"),trStart,trStep,leverage));
else
SetT(K_MODE,"MODE",StringFormat("Mode: Martingale | مارتینگل Lvl %d Avg %s Lots %.2f Trail %s",
martLevel,(basketAvg>0?Px(basketAvg):"--"),basketLots,basketTrailActive?"ON":"OFF"));
SetT(K_INFO,"INFO",StringFormat("%s %s %s Bid %s Ask %s Spr %d",(playing?"▶":"⏸"),SpeedTxt(speedMult),
TimeToString((datetime)simSec,TIME_DATE|TIME_SECONDS),Px(curBid),Px(curAsk),InpSpreadPts));
SetT(K_INFO2,"INFO2",StringFormat("TF %s | bar %d/%d | tick %d/%d | ⏱ %d:%02d.%d",
TFName(currentTF),cur+1,total,tkIdx,tkN,rm,rs,rt));
SetT(K_MSG,"MSG",lastMsg);
if(trOpen) RefreshTrades();
ChartRedraw(panelChart);
}
//+------------------------------------------------------------------+
//| Panel: framed, 3D-style buttons |
//+------------------------------------------------------------------+
color Lighten(const color c,const int a)
{
int r=(int)(((uint)c)&0xFF), g=(int)((((uint)c)>>8)&0xFF), b=(int)((((uint)c)>>16)&0xFF);
r=MathMin(255,r+a); g=MathMin(255,g+a); b=MathMin(255,b+a);
return (color)(r|(g<<8)|(b<<16));
}
void Rect(const string id,const int x,const int y,const int w,const int h,const color bg,const color border,const int z=0)
{
string n=P+id;
ObjectCreate(panelChart,n,OBJ_RECTANGLE_LABEL,0,0,0);
ObjectSetInteger(panelChart,n,OBJPROP_CORNER,CORNER_LEFT_UPPER);
ObjectSetInteger(panelChart,n,OBJPROP_XDISTANCE,x);
ObjectSetInteger(panelChart,n,OBJPROP_YDISTANCE,y);
ObjectSetInteger(panelChart,n,OBJPROP_XSIZE,w);
ObjectSetInteger(panelChart,n,OBJPROP_YSIZE,h);
ObjectSetInteger(panelChart,n,OBJPROP_BGCOLOR,bg);
ObjectSetInteger(panelChart,n,OBJPROP_BORDER_TYPE,BORDER_FLAT);
ObjectSetInteger(panelChart,n,OBJPROP_COLOR,border);
ObjectSetInteger(panelChart,n,OBJPROP_BACK,false);
ObjectSetInteger(panelChart,n,OBJPROP_SELECTABLE,false);
ObjectSetInteger(panelChart,n,OBJPROP_ZORDER,z);
}
void Reg(const string id,const int x,const int y,const int w,const int h)
{
if(rN>=48) return;
rId[rN]=id; rX[rN]=x; rY[rN]=y; rW[rN]=w; rH[rN]=h; rN++;
}
void RawBtn(const string n,const string t,const int x,const int y,const int w,const int h,
const color bg,const color tc,const color border,const int fs,const int z)
{
ObjectCreate(panelChart,n,OBJ_BUTTON,0,0,0);
ObjectSetInteger(panelChart,n,OBJPROP_XDISTANCE,x);
ObjectSetInteger(panelChart,n,OBJPROP_YDISTANCE,y);
ObjectSetInteger(panelChart,n,OBJPROP_XSIZE,w);
ObjectSetInteger(panelChart,n,OBJPROP_YSIZE,h);
ObjectSetString(panelChart,n,OBJPROP_TEXT,t);
ObjectSetString(panelChart,n,OBJPROP_FONT,"Segoe UI Semibold");
ObjectSetInteger(panelChart,n,OBJPROP_FONTSIZE,fs);
ObjectSetInteger(panelChart,n,OBJPROP_COLOR,tc);
ObjectSetInteger(panelChart,n,OBJPROP_BGCOLOR,bg);
ObjectSetInteger(panelChart,n,OBJPROP_BORDER_COLOR,border);
ObjectSetInteger(panelChart,n,OBJPROP_CORNER,CORNER_LEFT_UPPER);
ObjectSetInteger(panelChart,n,OBJPROP_SELECTABLE,false);
ObjectSetInteger(panelChart,n,OBJPROP_ZORDER,z);
ObjectSetInteger(panelChart,n,OBJPROP_STATE,false);
}
void Btn3D(const string id,const string t,const int x,const int y,const int w,const int h,const color bg,const int fs=9)
{
Rect(id+"_sh",x+2,y+3,w,h,C'6,8,14',C'6,8,14');
RawBtn(P+id,t,x,y,w,h,bg,clrWhite,Lighten(bg,55),fs,500);
Rect(id+"_hl",x+2,y+2,w-4,1,Lighten(bg,110),Lighten(bg,110));
Reg(id,x,y,w,h);
}
void SetBtn(const string id,const color bg,const color tc)
{
ObjectSetInteger(panelChart,P+id,OBJPROP_BGCOLOR,bg);
ObjectSetInteger(panelChart,P+id,OBJPROP_COLOR,tc);
ObjectSetInteger(panelChart,P+id,OBJPROP_BORDER_COLOR,Lighten(bg,55));
ObjectSetInteger(panelChart,P+id+"_hl",OBJPROP_BGCOLOR,Lighten(bg,110));
ObjectSetInteger(panelChart,P+id+"_hl",OBJPROP_COLOR,Lighten(bg,110));
}
void Lbl(const string id,const string t,const int x,const int y,const color c=clrYellow,
const int fs=9,const string font="Segoe UI")
{
string n=P+id;
ObjectCreate(panelChart,n,OBJ_LABEL,0,0,0);
ObjectSetInteger(panelChart,n,OBJPROP_XDISTANCE,x);
ObjectSetInteger(panelChart,n,OBJPROP_YDISTANCE,y);
ObjectSetString(panelChart,n,OBJPROP_TEXT,(t=="")?" ":t); // empty text would show "Label"
ObjectSetString(panelChart,n,OBJPROP_FONT,font);
ObjectSetInteger(panelChart,n,OBJPROP_COLOR,c);
ObjectSetInteger(panelChart,n,OBJPROP_CORNER,CORNER_LEFT_UPPER);
ObjectSetInteger(panelChart,n,OBJPROP_FONTSIZE,fs);
ObjectSetInteger(panelChart,n,OBJPROP_SELECTABLE,false);
ObjectSetInteger(panelChart,n,OBJPROP_ZORDER,0);
}
void EditBox(const string id,const string t,const int x,const int y,const int w,const int h)
{
string n=P+id;
ObjectCreate(panelChart,n,OBJ_EDIT,0,0,0);
ObjectSetInteger(panelChart,n,OBJPROP_XDISTANCE,x);
ObjectSetInteger(panelChart,n,OBJPROP_YDISTANCE,y);
ObjectSetInteger(panelChart,n,OBJPROP_XSIZE,w);
ObjectSetInteger(panelChart,n,OBJPROP_YSIZE,h);
ObjectSetString(panelChart,n,OBJPROP_TEXT,t);
ObjectSetString(panelChart,n,OBJPROP_FONT,"Segoe UI Semibold");
ObjectSetInteger(panelChart,n,OBJPROP_FONTSIZE,11);
ObjectSetInteger(panelChart,n,OBJPROP_COLOR,clrBlack);
ObjectSetInteger(panelChart,n,OBJPROP_BGCOLOR,clrWhite);
ObjectSetInteger(panelChart,n,OBJPROP_BORDER_COLOR,C'0,120,215');
ObjectSetInteger(panelChart,n,OBJPROP_CORNER,CORNER_LEFT_UPPER);
ObjectSetInteger(panelChart,n,OBJPROP_ALIGN,ALIGN_CENTER);
ObjectSetInteger(panelChart,n,OBJPROP_ZORDER,500);
}
void DateField(const string id,const datetime d,const int x,const int y)
{
RawBtn(P+id,TimeToString(d,TIME_DATE),x,y,104,26,clrWhite,clrBlack,C'0,120,215',10,500);
Reg(id,x,y,104,26);
Btn3D(id+"_I","▦ ▾",x+104,y,28,26,C'96,110,140',9);
}
void BuildPanel()
{
ObjectsDeleteAll(panelChart,P);
rN=0; calOpen=false; ctxOpen=false;
for(int i=0;i<40;i++){ lcC[i]=clrNONE; lcT[i]="~~"; }
const int PX=4, PY=4;
panW=480;
const int x=PX+14;
const color KEY=C'140,152,185', LAB=C'190,200,225';
int y=PY+34;
Rect("FRAME", PX, PY, panW, 400,C'24,30,46',C'0,150,255');
Rect("FRAME2",PX+3,PY+3,panW-6, 394,C'24,30,46',C'52,68,104');
Rect("TBAR", PX+4,PY+4,panW-8, 24, C'0,92,170',C'0,92,170');
Lbl("TTL","◆ MARKET REPLAY v9.1 | ریپلی بازار",x,PY+8,clrWhite,10,"Segoe UI Semibold");
// --- row 1: transport
Btn3D("PLAY", "▶ Play", x, y,104,30,clrDarkGreen,10);
Btn3D("PAUSE", "⏸ Pause", x+112, y,104,30,clrDarkOrange,10);
Btn3D("TRADES","▤ Trades",x+224, y,124,30,clrSteelBlue,10);
// --- row 2: speed slider
y+=40;
Lbl("LS","Speed",x,y+4,LAB,9);
slX=x+50; slY=y;
Rect("SL_TRK",slX,slY+SL_H/2-5,slW,10,C'208,213,226',C'96,108,140',600);
Rect("SL_GRV",slX+3,slY+SL_H/2-1,slW-6,2,C'120,132,165',C'120,132,165',600);
for(int i=0;i<5;i++)
{
string tx=(i==0)?"0.1x":(i==1)?"1x":(i==2)?"10x":(i==3)?"100x":"1000x";
int tx_x=slX+(int)MathRound(i*0.25*(slW-SL_TH))+SL_TH/2-(StringLen(tx)*3);
Lbl("SK"+IntegerToString(i),tx,tx_x,y+SL_H+3,KEY,7);
Rect("ST"+IntegerToString(i),slX+(int)MathRound(i*0.25*(slW-SL_TH))+SL_TH/2,slY+SL_H/2+6,1,4,C'96,108,140',C'96,108,140',600);
}
RawBtn(P+"SL_TH","",slX,slY,SL_TH,SL_H,C'238,241,248',clrBlack,C'70,80,115',8,700);
PlaceThumb();
Lbl("SPDV","Live 1x",x+322,y+3,clrGold,11,"Segoe UI Semibold");
Btn3D("LIVE","Live",x+400,y,52,26,clrTeal);
// --- row 3: timeframes
y+=50;
Lbl("LTF","TF",x,y+5,LAB);
Btn3D("TF_M1", "M1", x+26, y,48,26,clrSteelBlue);
Btn3D("TF_M5", "M5", x+78, y,48,26,clrSteelBlue);
Btn3D("TF_M15","M15", x+130, y,48,26,clrSteelBlue);
Btn3D("TF_M30","M30", x+182, y,48,26,clrSteelBlue);
Btn3D("TF_H1", "H1", x+234, y,48,26,clrSteelBlue);
Btn3D("TF_H4", "H4", x+286, y,48,26,clrSteelBlue);
Btn3D("TF_D1", "D1", x+338, y,48,26,clrSteelBlue);
Btn3D("TF_W1", "W1", x+390, y,48,26,clrSteelBlue);
// --- row 4: date range
y+=38;
dateRowY=y; fromFieldX=x+44; toFieldX=x+246;
Lbl("LF","From:",x,y+5,LAB);
DateField("FROM",selFrom,fromFieldX,y);
Lbl("LT","To:",x+206,y+5,LAB);
DateField("TO",selTo,toFieldX,y);
// --- row 5: lot + leverage
y+=38;
Lbl("LLOT","Lot:",x,y+5,LAB);
Btn3D("LOT_M","−",x+34, y,30,26,clrDimGray,12);
EditBox("LOT",DoubleToString(lotSize,2),x+68,y,78,26);
Btn3D("LOT_P","+",x+150,y,30,26,clrDimGray,12);
Btn3D("MAXLOT","Max",x+186,y,46,26,clrSlateGray,9);
Lbl("LLEV","Leverage 1 :",x+250,y+5,LAB);
EditBox("LEV",IntegerToString(leverage),x+346,y,70,26);
y+=31;
Lbl("LI","",x,y,KEY,8);
y+=22;
// --- row 6: SL / TP
Lbl("LSL","SL pts:",x,y+5,LAB);
EditBox("SLP",IntegerToString(slPts),x+50,y,76,26);
Lbl("LTP",(InpTradeMode==MODE_SINGLE)?"TP pts:":"Basket TP:",x+140,y+5,LAB);
EditBox("TPP",IntegerToString(tpPts),x+206,y,76,26);
Btn3D("APPLY","Apply to open",x+296,y,120,26,clrSteelBlue,9);
// --- row 6b: trailing stop
y+=36;
Lbl("LTR",(InpTradeMode==MODE_SINGLE)?"Trailing:":"Basket trail:",x,y+5,LAB);
Btn3D("TRAIL_T",trailOn?"ON":"OFF",x+84,y,52,26,trailOn?clrForestGreen:clrDimGray,9);
Lbl("LTS","Start pts:",x+146,y+5,LAB);
EditBox("TRS",IntegerToString(trStart),x+214,y,66,26);
Lbl("LTP2","Step pts:",x+290,y+5,LAB);
EditBox("TRP",IntegerToString(trStep),x+354,y,66,26);
// --- row 7: balance control
y+=36;
Lbl("LBV","Amount:",x,y+5,LAB);
EditBox("BALV",DoubleToString(resetBal,2),x+56,y,110,26);
Btn3D("BAL_RESET","Reset balance",x+174,y,130,26,clrDarkOrange,9);
Btn3D("BAL_DEP","+ Deposit",x+312,y,110,26,clrTeal,9);
// --- row 8: SELL (left) CLOSE BUY (right)
y+=40;
Btn3D("SELL", "SELL", x, y,166,46,clrFireBrick,12);
Btn3D("CLOSE","Close All",x+174, y,104,46,clrDimGray,10);
Btn3D("BUY", "BUY", x+286, y,166,46,clrForestGreen,12);
// --- stats block
y+=58;
int statTop=y-6;
Rect("STATS",x-6,statTop,panW-16,200,C'12,16,28',C'60,74,108');
Lbl("KB","BALANCE",x,y,KEY,8); Lbl("VB","",x,y+12,clrWhite,13,"Segoe UI Semibold");
Lbl("KE","EQUITY",x+156,y,KEY,8); Lbl("VE","",x+156,y+12,clrWhite,13,"Segoe UI Semibold");
Lbl("KF","FLOATING P/L",x+312,y,KEY,8); Lbl("VF","",x+312,y+12,clrWhite,13,"Segoe UI Semibold");
y+=42;
Lbl("KG","USED MARGIN",x,y,KEY,8); Lbl("VMG","",x,y+12,clrWhite,11,"Segoe UI Semibold");
Lbl("KH","FREE MARGIN",x+156,y,KEY,8); Lbl("VFM","",x+156,y+12,clrWhite,11,"Segoe UI Semibold");
Lbl("KL","MARGIN LEVEL",x+312,y,KEY,8); Lbl("VML","",x+312,y+12,clrWhite,11,"Segoe UI Semibold");
y+=40;
Lbl("POS","",x,y,clrWhite,9);
y+=22;
Rect("SEP1",x,y-2,panW-28,1,C'60,74,108',C'60,74,108');
Lbl("KT","TODAY",x,y+2,KEY,8); Lbl("VT","",x,y+14,clrWhite,11,"Segoe UI Semibold");
Lbl("KY","YESTERDAY",x+114,y+2,KEY,8); Lbl("VY","",x+114,y+14,clrWhite,11,"Segoe UI Semibold");
Lbl("KW","WEEK",x+228,y+2,KEY,8); Lbl("VW","",x+228,y+14,clrWhite,11,"Segoe UI Semibold");
Lbl("KM","MONTH",x+342,y+2,KEY,8); Lbl("VM","",x+342,y+14,clrWhite,11,"Segoe UI Semibold");
y+=40;
Lbl("LOTS","",x,y,LAB,9); y+=20;
Lbl("MODE","",x,y,LAB,9); y+=22;
Rect("SEP2",x,y-2,panW-28,1,C'60,74,108',C'60,74,108');
Lbl("INFO","",x,y+2,clrWhite,9); y+=19;
Lbl("INFO2","",x,y+2,clrWhite,9); y+=19;
Lbl("MSG","",x,y+2,clrTomato,9); y+=19;
Lbl("HINT","Keys: B=Buy S=Sell C=Close Space=Play/Pause +/-=Speed Esc=Cancel",x,y+2,C'110,124,160',8);
y+=26;
panH=y-PY;
ObjectSetInteger(panelChart,P+"FRAME", OBJPROP_YSIZE,panH);
ObjectSetInteger(panelChart,P+"FRAME2",OBJPROP_YSIZE,panH-6);
ObjectSetInteger(panelChart,P+"STATS", OBJPROP_YSIZE,y-26-statTop+22);
}
void SpBtn(const string id,const bool on,const color idle)
{
if(on) SetBtn(id,clrGold,clrBlack); else SetBtn(id,idle,clrWhite);
}
void TfBtn(const string id,const ENUM_TIMEFRAMES tf)
{
if(currentTF==tf) SetBtn(id,clrDodgerBlue,clrWhite); else SetBtn(id,clrSteelBlue,clrWhite);
}
void UpdateButtonStates(const bool redraw)
{
SetBtn("PLAY", playing?clrLimeGreen:clrDarkGreen, playing?clrBlack:clrWhite);
SetBtn("PAUSE",playing?clrDarkOrange:clrOrange, clrWhite);
ObjectSetString(panelChart,P+"PLAY", OBJPROP_TEXT,playing?"▶ Playing":"▶ Play");
ObjectSetString(panelChart,P+"PAUSE",OBJPROP_TEXT,playing?"⏸ Pause":"⏸ Paused");
if(trOpen) SetBtn("TRADES",clrDodgerBlue,clrWhite); else SetBtn("TRADES",clrSteelBlue,clrWhite);
SpBtn("LIVE",speedMult==1.0,clrTeal);
SetBtn("TRAIL_T",trailOn?clrForestGreen:clrDimGray,clrWhite);
ObjectSetString(panelChart,P+"TRAIL_T",OBJPROP_TEXT,trailOn?"ON":"OFF");
TfBtn("TF_M1",PERIOD_M1); TfBtn("TF_M5",PERIOD_M5); TfBtn("TF_M15",PERIOD_M15);
TfBtn("TF_M30",PERIOD_M30); TfBtn("TF_H1",PERIOD_H1); TfBtn("TF_H4",PERIOD_H4);
TfBtn("TF_D1",PERIOD_D1); TfBtn("TF_W1",PERIOD_W1);
SetT(K_SPEED,"SPDV",SpeedTxt(speedMult));
if(redraw) ChartRedraw(panelChart);
}
//+------------------------------------------------------------------+
//| Calendar popup (month grid, Sun..Sat, like a Windows date picker)|
//+------------------------------------------------------------------+
void CalBtn(const string id,const string t,const int x,const int y,const int w,const int h,
const color bg,const color tc,const color border,const int fs=9)
{
RawBtn(P+"CAL_"+id,t,x,y,w,h,bg,tc,border,fs,2000);
}
void OpenCal(const int target)
{
CloseCal();
calTarget=target; calOpen=true;
datetime d=(target==0)?selFrom:selTo;
MqlDateTime s; TimeToStruct(d,s);
calYear=s.year; calMon=s.mon;
int fx=(target==0)?fromFieldX:toFieldX;
calX=MathMin(fx,4+panW-calW-6);
calY=dateRowY+28;
Rect("CAL_BG",calX,calY,calW,calH,clrWhite,C'110,110,110',1999);
CalBtn("PREV","◀",calX+6,calY+4,28,24,clrWhite,C'60,60,60',clrWhite,10);
CalBtn("NEXT","▶",calX+calW-34,calY+4,28,24,clrWhite,C'60,60,60',clrWhite,10);
CalBtn("TITLE","",calX+40,calY+4,calW-80,24,clrWhite,clrBlack,clrWhite,10);
string wd[7]={"Sun","Mon","Tue","Wed","Thu","Fri","Sat"};
int gx=calX+8, gy=calY+CAL_HDR+CAL_WK;
for(int c=0;c<7;c++)
CalBtn("W"+IntegerToString(c),wd[c],gx+c*CAL_CW,calY+CAL_HDR,CAL_CW,CAL_WK,clrWhite,C'40,40,40',clrWhite,9);
for(int i=0;i<42;i++)
CalBtn("D"+IntegerToString(i),"",gx+(i%7)*CAL_CW+1,gy+(i/7)*CAL_CH+1,CAL_CW-2,CAL_CH-2,clrWhite,clrBlack,clrWhite,10);
CalBtn("TODAY","Today: "+TimeToString(DayStart(TimeLocal()),TIME_DATE),gx,gy+6*CAL_CH+4,210,24,clrWhite,C'30,30,30',C'0,120,215',9);
RefreshCal();
ChartRedraw(panelChart);
}
void RefreshCal()
{
ObjectSetString(panelChart,P+"CAL_TITLE",OBJPROP_TEXT,MonName[calMon-1]+" "+IntegerToString(calYear));
datetime first=StringToTime(StringFormat("%04d.%02d.01",calYear,calMon));
MqlDateTime f; TimeToStruct(first,f);
datetime start=first-(datetime)((long)f.day_of_week*86400);
datetime sel=(calTarget==0)?selFrom:selTo;
datetime today=DayStart(TimeLocal());
for(int i=0;i<42;i++)
{
datetime d=start+(datetime)((long)i*86400);
MqlDateTime m; TimeToStruct(d,m);
bool inMon=(m.mon==calMon);
bool on=(d>=dataFirst && d<=dataLast);
calDate[i]=d; calOn[i]=on;
string n=P+"CAL_D"+IntegerToString(i);
ObjectSetString(panelChart,n,OBJPROP_TEXT,IntegerToString(m.day));
color tc=!on?C'205,205,205':(inMon?clrBlack:C'140,140,140');
color bg=(d==sel)?C'204,228,247':clrWhite;
color bd=(d==sel)?C'0,120,215':((d==today)?clrCrimson:clrWhite);
ObjectSetInteger(panelChart,n,OBJPROP_COLOR,tc);
ObjectSetInteger(panelChart,n,OBJPROP_BGCOLOR,bg);
ObjectSetInteger(panelChart,n,OBJPROP_BORDER_COLOR,bd);
}
}
void CloseCal()
{
if(!calOpen) return;
ObjectsDeleteAll(panelChart,P+"CAL_");
calOpen=false;
ChartRedraw(panelChart);
}
void PickDate(const datetime d)
{
if(calTarget==0){ selFrom=d; if(selTo<selFrom) selTo=selFrom; }
else { selTo=d; if(selFrom>selTo) selFrom=selTo; }
selDirty=true;
ObjectSetString(panelChart,P+"FROM",OBJPROP_TEXT,TimeToString(selFrom,TIME_DATE));
ObjectSetString(panelChart,P+"TO", OBJPROP_TEXT,TimeToString(selTo,TIME_DATE));
lastMsg="Range set - press Play to apply | محدوده انتخاب شد، Play را بزنید";
CloseCal();
}
//+------------------------------------------------------------------+
//| Trades window: floating, draggable, resizable, MT5-like table |
//+------------------------------------------------------------------+
double twFr[10]={0.09,0.08,0.15,0.10,0.07,0.11,0.10,0.10,0.10,0.10};
void TwReg(const string n,const int ox,const int oy)
{
if(twCnt>=600) return;
twN[twCnt]=n; twOX[twCnt]=ox; twOY[twCnt]=oy; twCnt++;
}
void TwRect(const string id,const int ox,const int oy,const int w,const int h,const color bg,const color bd)
{
Rect(id,trX+ox,trY+oy,w,h,bg,bd,3000);
TwReg(P+id,ox,oy);
}
void TwLbl(const string id,const string t,const int ox,const int oy,const color c,const int fs,const bool right,
const string font="Segoe UI")
{
string n=P+id;
ObjectCreate(panelChart,n,OBJ_LABEL,0,0,0);
ObjectSetInteger(panelChart,n,OBJPROP_CORNER,CORNER_LEFT_UPPER);
ObjectSetInteger(panelChart,n,OBJPROP_ANCHOR,right?ANCHOR_RIGHT_UPPER:ANCHOR_LEFT_UPPER);
ObjectSetInteger(panelChart,n,OBJPROP_XDISTANCE,trX+ox);
ObjectSetInteger(panelChart,n,OBJPROP_YDISTANCE,trY+oy);
ObjectSetString(panelChart,n,OBJPROP_TEXT,(t=="")?" ":t); // empty text would show "Label"
ObjectSetString(panelChart,n,OBJPROP_FONT,font);
ObjectSetInteger(panelChart,n,OBJPROP_FONTSIZE,fs);
ObjectSetInteger(panelChart,n,OBJPROP_COLOR,c);
ObjectSetInteger(panelChart,n,OBJPROP_SELECTABLE,false);
ObjectSetInteger(panelChart,n,OBJPROP_ZORDER,0);
TwReg(n,ox,oy);
}
void BuildTrades()
{
ObjectsDeleteAll(panelChart,P+"TW_");
twCnt=0; trOpen=true;
int cw=(int)ChartGetInteger(panelChart,CHART_WIDTH_IN_PIXELS);
int ch=(int)ChartGetInteger(panelChart,CHART_HEIGHT_IN_PIXELS);
trW=MathMax(600,MathMin(trW,cw-4)); trH=MathMax(160,MathMin(trH,ch-4));
trX=MathMax(0,MathMin(trX,cw-trW)); trY=MathMax(0,MathMin(trY,ch-trH));
twVis=MathMax(1,MathMin(TR_MAXROWS,(trH-TR_TITLE-TR_HEAD-TR_FOOT)/TR_ROWH));
for(int i=0;i<500;i++){ twC[i]="~~"; twCC[i]=clrNONE; }
for(int i=36;i<40;i++){ lcT[i]="~~"; lcC[i]=clrNONE; }
TwRect("TW_BG",0,0,trW,trH,C'252,252,254',C'80,92,130');
TwRect("TW_TBAR",1,1,trW-2,TR_TITLE-1,C'0,92,170',C'0,92,170');
TwLbl("TW_TTL","Trades | معاملات",8,4,clrWhite,9,false,"Arial Bold");
TwLbl("TW_XC","✕",trW-20,3,clrWhite,10,false);
TwRect("TW_HD",1,TR_TITLE,trW-2,TR_HEAD,C'208,216,236',C'160,172,204');
// bold, large, dark column titles
string hd[10]={"Symbol","Ticket","Time","Type","Volume","Price","S / L","T / P","Price","Profit"};
double tw=(double)(trW-2-30);
double acc=1.0;
double colR[10]; double colL[10];
for(int c=0;c<10;c++){ colL[c]=acc; acc+=twFr[c]*tw; colR[c]=acc; }
for(int c=0;c<10;c++)
TwLbl("TW_H"+IntegerToString(c),hd[c],(int)(c<4?colL[c]+6:colR[c]-6),TR_TITLE+6,C'10,20,70',10,c>=4,"Arial Bold");
for(int r=0;r<twVis;r++)
{
int yy=TR_TITLE+TR_HEAD+r*TR_ROWH;
color bg=(r%2==0)?C'255,255,255':C'243,246,251';
TwRect("TW_S"+IntegerToString(r),1,yy,trW-2,TR_ROWH,bg,bg);
for(int c=0;c<10;c++)
TwLbl("TW_C"+IntegerToString(r)+"_"+IntegerToString(c)," ",(int)(c<4?colL[c]+6:colR[c]-6),yy+3,C'30,36,60',9,c>=4);
TwLbl("TW_C"+IntegerToString(r)+"_10"," ",trW-24,yy+2,C'200,40,40',10,false);
}
// footer: big bold Balance / Equity / Profit
int fy=trH-TR_FOOT;
TwRect("TW_FT",1,fy,trW-2,TR_FOOT-1,C'208,216,236',C'160,172,204');
TwLbl("TW_FBK","Balance:",10,fy+8,C'40,50,90',10,false,"Arial Bold");
TwLbl("TW_FBV"," ",84,fy+7,C'8,16,60',12,false,"Arial Bold");
TwLbl("TW_FEK","Equity:",210,fy+8,C'40,50,90',10,false,"Arial Bold");
TwLbl("TW_FEV"," ",272,fy+7,C'8,16,60',12,false,"Arial Bold");
TwLbl("TW_FPK","Profit:",390,fy+8,C'40,50,90',10,false,"Arial Bold");
TwLbl("TW_FPV"," ",446,fy+7,C'0,130,50',12,false,"Arial Bold");
TwLbl("TW_UP","▲",trW-72,fy+8,C'70,80,115',10,false);
TwLbl("TW_DN","▼",trW-46,fy+8,C'70,80,115',10,false);
TwLbl("TW_GR","◢",trW-15,trH-17,C'110,120,150',9,false);
}
void TwSet(const int r,const int c,const string txt0,const color col)
{
int k=r*11+c;
if(k>=500) return;
string txt=(txt0=="")?" ":txt0; // never leave a label empty
string n=P+"TW_C"+IntegerToString(r)+"_"+IntegerToString(c);
if(twC[k]!=txt){ twC[k]=txt; ObjectSetString(panelChart,n,OBJPROP_TEXT,txt); }
if(twCC[k]!=col){ twCC[k]=col; ObjectSetInteger(panelChart,n,OBJPROP_COLOR,col); }
}
void RefreshTrades()
{
if(!trOpen) return;
twRows=0;
for(int i=0;i<ArraySize(pos);i++) if(pos[i].open_ && twRows<800){ twKind[twRows]=0; twIdx[twRows]=i; twRows++; }
for(int i=0;i<ArraySize(ords);i++) if(ords[i].live && twRows<800){ twKind[twRows]=1; twIdx[twRows]=i; twRows++; }
trTop=MathMax(0,MathMin(trTop,MathMax(0,twRows-twVis)));
const color TX=C'30,36,60', BUYC=C'0,90,200', SELLC=C'200,40,40', GRN=C'0,140,60', RED=C'200,40,40';
for(int r=0;r<twVis;r++)
{
int row=trTop+r;
if(row>=twRows){ for(int c=0;c<=10;c++) TwSet(r,c,"",TX); continue; }
if(twKind[row]==0)
{
int i=twIdx[row];
double pr=(pos[i].dir>0)?curBid:curAsk;
double fl=CalcProfit(pos[i].dir,pos[i].open,pos[i].lots,pr);
double ssl=pos[i].sl, stp=pos[i].tp;
if(InpTradeMode==MODE_MARTINGALE){ ssl=basketTrailActive?basketSL:0; stp=basketTP; }
TwSet(r,0,baseSym,TX);
TwSet(r,1,IntegerToString((long)pos[i].ticket),TX);
TwSet(r,2,TimeToString(pos[i].time,TIME_DATE|TIME_SECONDS),TX);
TwSet(r,3,(pos[i].dir>0)?"buy":"sell",(pos[i].dir>0)?BUYC:SELLC);
TwSet(r,4,DoubleToString(pos[i].lots,2),TX);
TwSet(r,5,Px(pos[i].open),TX);
TwSet(r,6,ssl>0?Px(ssl):"",TX);
TwSet(r,7,stp>0?Px(stp):"",TX);
TwSet(r,8,Px(pr),TX);
TwSet(r,9,Money(fl),(fl>=0)?GRN:RED);
TwSet(r,10,"✕",RED);
}
else
{
int i=twIdx[row];
int t=ords[i].type;
bool buy=(t==1||t==3);
TwSet(r,0,baseSym,TX);
TwSet(r,1,IntegerToString((long)ords[i].ticket),TX);
TwSet(r,2,TimeToString(ords[i].time,TIME_DATE|TIME_SECONDS),TX);
TwSet(r,3,OrdTypeName(t),buy?BUYC:SELLC);
TwSet(r,4,DoubleToString(ords[i].lots,2),TX);
TwSet(r,5,Px(ords[i].price),TX);
TwSet(r,6,"",TX); TwSet(r,7,"",TX);
TwSet(r,8,Px(buy?curAsk:curBid),TX);
TwSet(r,9,"",TX);
TwSet(r,10,"✕",RED);
}
}
// footer values: large, bold, strong colours
string vb=Money(bal), ve=Money(eq), vp=MoneyS(eq-bal);
color pc=(eq-bal>=0)?C'0,130,50':C'200,30,30';
if(lcT[36]!=vb){ lcT[36]=vb; ObjectSetString(panelChart,P+"TW_FBV",OBJPROP_TEXT,vb); }
if(lcT[37]!=ve){ lcT[37]=ve; ObjectSetString(panelChart,P+"TW_FEV",OBJPROP_TEXT,ve); }
if(lcT[38]!=vp){ lcT[38]=vp; ObjectSetString(panelChart,P+"TW_FPV",OBJPROP_TEXT,vp); }
if(lcC[38]!=pc){ lcC[38]=pc; ObjectSetInteger(panelChart,P+"TW_FPV",OBJPROP_COLOR,pc); }
}
void OpenTrades()
{
trOpen=true;
BuildTrades();
RefreshTrades();
ChartRedraw(panelChart);
}
void CloseTrades()
{
trOpen=false; trDrag=false; trSize=false;
ObjectsDeleteAll(panelChart,P+"TW_");
twCnt=0;
ChartSetInteger(panelChart,CHART_MOUSE_SCROLL,true);
ChartRedraw(panelChart);
}