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