| //+------------------------------------------------------------------+
|
| //| 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);
|
| }
|
|
|