//+------------------------------------------------------------------+ //| MarketProfileTAP.mq5 | //| Time-at-Price (TAP) Market Profile v1.0 | //| | //| Core mechanic: | //| A 1-second timer fires every second. Price is sampled ONCE | //| and ONE unit of time is credited to the corresponding price | //| bucket. Rapid price oscillation within a second counts as | //| ONE second — not many ticks. The result is a profile that | //| shows WHERE price spent time, not how frantically it moved. | //| | //| Rolling window: credits older than InpWindowHours are expired | //| from both the circular buffer and the live histogram. | //| | //| Persistence: the buffer is saved to a binary file on deinit | //| and reloaded on init so the profile survives indicator reloads. | //| It does NOT fill gaps from while the terminal was closed. | //+------------------------------------------------------------------+ #property copyright "TAP Market Profile" #property version "1.00" #property description "Time-at-Price Market Profile | 1s sampling | rolling window" #property indicator_chart_window #property indicator_plots 0 //------------------------------------------------------------------- // Enums //------------------------------------------------------------------- enum ENUM_TAP_PRICE { TAP_BID = 0, // Bid TAP_ASK = 1, // Ask TAP_MID = 2 // Mid (bid+ask)/2 }; enum ENUM_TAP_ASSIGN { TAP_LOWER = 0, // Lower bucket (floor) TAP_UPPER = 1, // Upper bucket (ceiling of lower → next bucket up) TAP_SPLIT = 2 // Split 50/50 between lower and upper bucket }; enum ENUM_TAP_DISPLAY { TAP_BANDS = 0, // Full-width horizontal bands (like heatmap) TAP_PANEL = 1, // Right-side panel bars (classic market profile) TAP_BOTH = 2 // Both: bands behind + panel on right }; //------------------------------------------------------------------- // Inputs //------------------------------------------------------------------- input ENUM_TAP_PRICE InpPriceSource = TAP_MID; // Price source input ENUM_TAP_ASSIGN InpBucketAssign = TAP_LOWER; // Bucket assignment input int InpBucketPts = 10; // Bucket size (points) input int InpWindowHours = 8; // Rolling window (hours) input ENUM_TAP_DISPLAY InpDisplayMode = TAP_BOTH; // Display style input int InpPanelBars = 40; // Panel max width (chart bars) input int InpFutureBars = 10; // Right extension (bars, bands mode) input bool InpShowPOC = true; // Highlight POC input bool InpShowVA = true; // Show Value Area lines input double InpVAPct = 70.0; // Value Area % input color InpColLow = clrBlack; // Colour – low time input color InpColMid = clrTeal; // Colour – mid time input color InpColHigh = clrCrimson; // Colour – high time input color InpColPOC = clrYellow; // Colour – POC line input color InpColVA = clrGoldenrod; // Colour – VA lines input bool InpPersist = true; // Save/load data to file //------------------------------------------------------------------- // Sizing limits // MAX_BUF = 2 × 24h × 3600 s (factor-2 for split mode overhead) // MAX_HIST = max distinct price buckets tracked simultaneously //------------------------------------------------------------------- #define MAX_BUF 172800 #define MAX_HIST 4000 //------------------------------------------------------------------- // Object name prefixes / singletons //------------------------------------------------------------------- #define PFX_BAND "TAP_b_" #define PFX_PNL "TAP_p_" #define OBJ_POC "TAP_poc" #define OBJ_VAH "TAP_vah" #define OBJ_VAL "TAP_val" #define OBJ_LBL "TAP_lbl" //------------------------------------------------------------------- // Circular buffer – rolling window of sampled seconds //------------------------------------------------------------------- datetime g_bTs [MAX_BUF]; // sample timestamp (GMT) double g_bKey[MAX_BUF]; // bucket lower-boundary price float g_bWt [MAX_BUF]; // weight: 1.0 or 0.5 (split) int g_bHead = 0; // index of oldest valid entry int g_bTail = 0; // index of next write slot int g_bCount = 0; // valid entries in use //------------------------------------------------------------------- // Live histogram – sorted by bucket key for O(log n) lookup //------------------------------------------------------------------- double g_hKey[MAX_HIST]; // bucket lower-boundary price (sorted asc) double g_hCnt[MAX_HIST]; // accumulated seconds (fractional for split) int g_hSz = 0; // number of active buckets //------------------------------------------------------------------- // Derived config + state //------------------------------------------------------------------- double g_bucketSize = 0.0; // bucket height in price units int g_windowSecs = 0; // InpWindowHours in seconds datetime g_lastSample = 0; // timestamp of last successful sample //+------------------------------------------------------------------+ //| Delete chart objects whose name begins with prefix | //+------------------------------------------------------------------+ void PurgePrefix(const string pfx) { for(int i = ObjectsTotal(0,0,-1)-1; i >= 0; i--) { string n = ObjectName(0,i,0,-1); if(StringFind(n, pfx) == 0) ObjectDelete(0, n); } } //+------------------------------------------------------------------+ //| Three-stop colour blend | //+------------------------------------------------------------------+ color ThermalColor(double ratio) { ratio = MathMax(0.0, MathMin(1.0, ratio)); color c1, c2; double t; if(ratio < 0.5){ c1=InpColLow; c2=InpColMid; t=ratio*2.0; } else { c1=InpColMid; c2=InpColHigh; t=(ratio-0.5)*2.0; } int r=(int)(( c1 &0xFF)*(1-t)+( c2 &0xFF)*t); int g=(int)(((c1>> 8)&0xFF)*(1-t)+((c2>> 8)&0xFF)*t); int b=(int)(((c1>>16)&0xFF)*(1-t)+((c2>>16)&0xFF)*t); return (color)(r|(g<<8)|(b<<16)); } //+------------------------------------------------------------------+ //| Upsert a filled rectangle | //+------------------------------------------------------------------+ void UpsertRect(const string nm, datetime t1, double p1, datetime t2, double p2, color clr, bool back) { if(ObjectFind(0,nm)<0) ObjectCreate(0,nm,OBJ_RECTANGLE,0,t1,p1,t2,p2); ObjectMove(0,nm,0,t1,p1); ObjectMove(0,nm,1,t2,p2); ObjectSetInteger(0,nm,OBJPROP_COLOR, clr); ObjectSetInteger(0,nm,OBJPROP_FILL, true); ObjectSetInteger(0,nm,OBJPROP_STYLE, STYLE_SOLID); ObjectSetInteger(0,nm,OBJPROP_WIDTH, 1); ObjectSetInteger(0,nm,OBJPROP_BACK, back); ObjectSetInteger(0,nm,OBJPROP_SELECTABLE, false); ObjectSetInteger(0,nm,OBJPROP_HIDDEN, true); } //+------------------------------------------------------------------+ //| Upsert a horizontal line | //+------------------------------------------------------------------+ void UpsertHLine(const string nm, double price, color clr, int w, ENUM_LINE_STYLE sty) { if(ObjectFind(0,nm)<0) ObjectCreate(0,nm,OBJ_HLINE,0,0,price); ObjectSetDouble (0,nm,OBJPROP_PRICE, price); ObjectSetInteger(0,nm,OBJPROP_COLOR, clr); ObjectSetInteger(0,nm,OBJPROP_WIDTH, w); ObjectSetInteger(0,nm,OBJPROP_STYLE, sty); ObjectSetInteger(0,nm,OBJPROP_BACK, false); ObjectSetInteger(0,nm,OBJPROP_SELECTABLE, false); ObjectSetInteger(0,nm,OBJPROP_HIDDEN, true); } //------------------------------------------------------------------- // H I S T O G R A M H E L P E R S //------------------------------------------------------------------- //+------------------------------------------------------------------+ //| Binary search. Returns index if found, -(ins+1) if missing. | //+------------------------------------------------------------------+ int HistFind(double key) { double eps = g_bucketSize * 0.001; int lo=0, hi=g_hSz-1; while(lo<=hi) { int mid=(lo+hi)/2; if(MathAbs(g_hKey[mid]-key)=0){ g_hCnt[idx]+=w; return; } int ins=-idx-1; if(g_hSz>=MAX_HIST) return; for(int i=g_hSz;i>ins;i--){ g_hKey[i]=g_hKey[i-1]; g_hCnt[i]=g_hCnt[i-1]; } g_hKey[ins]=key; g_hCnt[ins]=w; g_hSz++; } //+------------------------------------------------------------------+ //| Remove weight from bucket – removes bucket when it reaches zero | //+------------------------------------------------------------------+ void HistRemove(double key, double w) { int idx=HistFind(key); if(idx<0) return; g_hCnt[idx]-=w; if(g_hCnt[idx]<1e-6) { for(int i=idx;i=MAX_BUF) { HistRemove(g_bKey[g_bHead], g_bWt[g_bHead]); g_bHead=(g_bHead+1)%MAX_BUF; g_bCount--; } g_bTs [g_bTail]=ts; g_bKey[g_bTail]=key; g_bWt [g_bTail]=(float)wt; g_bTail=(g_bTail+1)%MAX_BUF; g_bCount++; HistAdd(key, wt); } //+------------------------------------------------------------------+ //| Remove all buffer entries older than the rolling window | //+------------------------------------------------------------------+ void BufExpire() { datetime cutoff=TimeGMT()-(datetime)g_windowSecs; while(g_bCount>0 && g_bTs[g_bHead]=cutoff) BufPush(ts,key,wt); // only load if still in window } FileClose(fh); } //------------------------------------------------------------------- // D R A W I N G //------------------------------------------------------------------- //+------------------------------------------------------------------+ //| Find POC index in histogram | //+------------------------------------------------------------------+ int FindPOC() { int poc=0; for(int i=1;ig_hCnt[poc]) poc=i; return poc; } //+------------------------------------------------------------------+ //| Compute Value Area – returns true and fills vaLo/vaHi | //+------------------------------------------------------------------+ bool CalcVA(int pocIdx, int &vaLo, int &vaHi) { if(g_hSz<=0) return false; double tot=0.0; for(int i=0;i0) ?g_hCnt[vaLo-1]:0.0; if(aHi==0.0&&aLo==0.0) break; if(aHi>=aLo){vaHi++;acc+=g_hCnt[vaHi];} else {vaLo--;acc+=g_hCnt[vaLo];} } return true; } //+------------------------------------------------------------------+ //| Full redraw | //+------------------------------------------------------------------+ void DrawAll() { if(g_hSz<=0) { // Nothing sampled yet – show a waiting label and exit if(ObjectFind(0,OBJ_LBL)<0) ObjectCreate(0,OBJ_LBL,OBJ_LABEL,0,0,0); ObjectSetInteger(0,OBJ_LBL,OBJPROP_CORNER, CORNER_LEFT_UPPER); ObjectSetInteger(0,OBJ_LBL,OBJPROP_XDISTANCE,6); ObjectSetInteger(0,OBJ_LBL,OBJPROP_YDISTANCE,4); ObjectSetString (0,OBJ_LBL,OBJPROP_TEXT, "TAP Market Profile – sampling… (0 s recorded)"); ObjectSetInteger(0,OBJ_LBL,OBJPROP_COLOR, clrSilver); ObjectSetInteger(0,OBJ_LBL,OBJPROP_FONTSIZE, 8); ObjectSetInteger(0,OBJ_LBL,OBJPROP_SELECTABLE,false); ObjectSetInteger(0,OBJ_LBL,OBJPROP_HIDDEN, true); ChartRedraw(0); return; } // ----- shared geometry ----------------------------------------- int totalBars=Bars(_Symbol,PERIOD_CURRENT); if(totalBars<2) return; long firstVis =ChartGetInteger(0,CHART_FIRST_VISIBLE_BAR); int barLeft =(int)MathMin(firstVis,totalBars-2); datetime tLeft =iTime(_Symbol,PERIOD_CURRENT,barLeft); datetime tRight =iTime(_Symbol,PERIOD_CURRENT,0); if(tLeft==0||tRight==0) return; int pSec =PeriodSeconds(PERIOD_CURRENT); datetime tEnd =tRight+(datetime)(InpFutureBars*pSec); datetime panelR =tRight+(datetime)(InpFutureBars*pSec); datetime panelMax=(datetime)(InpPanelBars*pSec); // ----- max count for normalisation ---------------------------- double maxCnt=0.0; for(int i=0;imaxCnt) maxCnt=g_hCnt[i]; if(maxCnt<=0.0) return; // ----- POC / VA ----------------------------------------------- int pocIdx=FindPOC(); int vaLo=pocIdx,vaHi=pocIdx; if(InpShowVA) CalcVA(pocIdx,vaLo,vaHi); // ----- purge stale objects ------------------------------------ PurgePrefix(PFX_BAND); PurgePrefix(PFX_PNL); // ----- draw each bucket --------------------------------------- for(int i=0;i=vaLo && i<=vaHi) drawR=MathMin(1.0,ratio*1.15); color clr=ThermalColor(drawR); // --- BANDS mode --- if(InpDisplayMode==TAP_BANDS || InpDisplayMode==TAP_BOTH) UpsertRect(PFX_BAND+IntegerToString(i), tLeft,pBot,tEnd,pTop,clr,true); // --- PANEL mode --- if(InpDisplayMode==TAP_PANEL || InpDisplayMode==TAP_BOTH) { datetime barW =(datetime)(ratio*(double)panelMax); datetime barL =panelR-barW; // In BOTH mode use a brighter version for the panel overlay color pClr=(InpDisplayMode==TAP_BOTH)?ThermalColor(MathMin(1.0,ratio*1.4)):clr; UpsertRect(PFX_PNL+IntegerToString(i), barL,pBot,panelR,pTop,pClr,false); } } // ----- POC line ----------------------------------------------- if(InpShowPOC) UpsertHLine(OBJ_POC, g_hKey[pocIdx]+g_bucketSize*0.5, InpColPOC, 2, STYLE_SOLID); else ObjectDelete(0,OBJ_POC); // ----- VA lines ----------------------------------------------- if(InpShowVA) { UpsertHLine(OBJ_VAH, g_hKey[vaHi]+g_bucketSize, InpColVA, 1, STYLE_DASH); UpsertHLine(OBJ_VAL, g_hKey[vaLo], InpColVA, 1, STYLE_DASH); } else{ ObjectDelete(0,OBJ_VAH); ObjectDelete(0,OBJ_VAL); } // ----- info label --------------------------------------------- if(ObjectFind(0,OBJ_LBL)<0) ObjectCreate(0,OBJ_LBL,OBJ_LABEL,0,0,0); double pocPrice=g_hKey[pocIdx]+g_bucketSize*0.5; double totalSec=0.0; for(int i=0;i 0"); return INIT_PARAMETERS_INCORRECT; } if(InpWindowHours<=0 || InpWindowHours>240) { Alert("TAP: InpWindowHours must be 1-240"); return INIT_PARAMETERS_INCORRECT; } g_bucketSize = InpBucketPts * _Point; g_windowSecs = InpWindowHours * 3600; // Warm the symbol data feed double tmp=SymbolInfoDouble(_Symbol,SYMBOL_BID); // Load persisted data (replays the buffer through the window filter) LoadBuffer(); BufExpire(); // prune anything older than window before first draw EventSetTimer(1); // 1-second heartbeat DrawAll(); return INIT_SUCCEEDED; } void OnDeinit(const int reason) { EventKillTimer(); SaveBuffer(); PurgePrefix(PFX_BAND); PurgePrefix(PFX_PNL); ObjectDelete(0,OBJ_POC); ObjectDelete(0,OBJ_VAH); ObjectDelete(0,OBJ_VAL); ObjectDelete(0,OBJ_LBL); ChartRedraw(0); } //+------------------------------------------------------------------+ //| 1-second heartbeat | //| 1. Sample price (the core TAP logic) | //| 2. Expire entries outside the rolling window | //| 3. Redraw | //+------------------------------------------------------------------+ void OnTimer() { SampleOnce(); BufExpire(); DrawAll(); } //+------------------------------------------------------------------+ //| OnCalculate – not used for logic but required by MQL5 | //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[], const int &spread[]) { return rates_total; } //+------------------------------------------------------------------+ //| Scroll / zoom triggers a visual refresh (no re-sampling) | //+------------------------------------------------------------------+ void OnChartEvent(const int id,const long &lp,const double &dp,const string &sp) { if(id==CHARTEVENT_CHART_CHANGE) DrawAll(); } //+------------------------------------------------------------------+